I’ve been running my own mail server at home for years using Postfix, dovecot, amavisd-new, ClamAV and SpamAssassin. But it requires a reliable connection and some maintenance once in a while. And of course it always breaks when I‘m on the other side of the world.
To free myself of that burden I decided to make the move to Office 365. I got myself a P1 subscription and started to browse through the configuration screens. The migration of an account from IMAP to Exchange Online was very fast and easy.
Happy with how everything looked, felt and connected I was ready to make the switch.
Just before I wanted to change the MX record to point to Office 365 I double checked the configuration of my account. I discovered I couldn’t find a way to set my account as a catch-all account. After some research I found out this is not possible at all!
“Catch-all Mailbox
A catch-all mailbox receives messages sent to email addresses in a domain that do not exist. Exchange Online anti-spam filters use recipient filtering to reject messages sent to mailboxes that don’t exist, so catch-all mailboxes are not supported.”
That left me 2 options:
- Stop the migration to Office 365, and leave things as they were.
- Make every email address I used in the past an alias.
I started searching if anyone has done this before. It looks like this is not the case, so seeing this as a challenge, I started working on my own solution.
Extracting all used addresses
First you need to get every email address ever used as a recipient.
As my bash scripting is a bit rusty, I found this script to convert mdir to mbox format by Joerg Reinhardt which I used as base for my own script: getting email recipient addresses from maildir.
The script needs 2 parameters, the maildir directory and the email domain you want to get the aliases for.
$ ./maildir-dump.sh <Maildir directory> <Email domain>
So for me:
$ ./maildir-dump.sh Maildir hompus.nl
This results in a long file with a lot of email addresses. To aggregate this list into a CSV file you can use the following command:
sort Maildir.dump | uniq -c | sort -k1nr | awk ‘BEGIN {OFS = “;”;} {print $1,$2}’ > Maildir.csv
This allows you to open the file using Excel and remove all entries you don’t want to be an alias.
I counted 385 unique email addresses, too many to add manually.
Adding the aliases to a mailbox
First I configured and connected to Exchange Online using the “Windows PowerShell to the Service” article.
First we will read the CSV file we have generated and authored.
$csv = Import-Csv D:\Maildir.csv -Delimiter ‘;’ -Header Count, Email
Then you need to get a reference to the mailbox you want to add the aliases to.
$temp = Get-Mailbox -Identity michael
You need to add all email addresses to the EmailAddress property, but I discovered the last email address to be added will become the default address. So make sure the primary SMTP address is added last.
$temp.EmailAddresses = $temp.EmailAddresses | ? { $_ -ne “SMTP:” + $temp.PrimarySmtpAddress }
$csv | % { $temp.EmailAddresses += (“SMTP:” + $_.Email) }
$temp.EmailAddresses += “SMTP:” + $temp.PrimarySmtpAddress
Now you only need to set the EmailAddress property on the actual mailbox
Set-Mailbox -Identity michael -EmailAddresses $temp.EmailAddresses
And done!
Recently I worked on an HttpHandler implementation that is serving images from a backend system. Although everything seemed to work as expected it was discovered images were requested by the browser on every page refresh instead of caching them locally. Together with my colleague Bert-Jan I investigated and solved the problem which will be explained in this post.
The problem
Let’s start with the original (simplified) code. This code gets the image from the backend system (in this case Content Management Server 2002) and serves it to the browser, or in case the resource is not available it will return "404 Not Found".
public class ResourceHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetMaxAge(new TimeSpan(1, 0, 0)); string imagePath = "some path"; Resource resource = CmsHttpContext.Current.RootResourceGallery .GetByRelativePath(imagePath) as Resource; if (resource == null) { // Resource not found context.Response.StatusCode = 404; return; } using (Stream stream = resource.OpenReadStream()) { byte[] buffer = new byte[32]; while (stream.Read(buffer, 0, 32) > 0) { context.Response.BinaryWrite(buffer); } } } public bool IsReusable { get { return true; } } }
Every time the browser requested a resource it responded with the following headers and included the full image.
HTTP/1.1 200 OK Cache-Control: public Content-Length: 3488 Content-Type: image/gif Server: Microsoft-IIS/6.0 X-AspNet-Version: 2.0.50727 COMMERCE-SERVER-SOFTWARE: Microsoft Commerce Server, Enterprise Edition X-Powered-By: ASP.NET Date: Fri, 11 Mar 2011 10:51:08 GMT GIF89a... (the raw image)
The CAUSE
For some reason the local browser cache was omitted. We fired up Fiddler and started comparing the headers to an other source where an image was getting cached locally.
On the first request we discovered an additional header "Last Modified":
Last-Modified: Tue, 05 Jun 2007 15:19:48 GMT
The second response we got to the same image resulted not in a "200 OK" but a "304 Not Modified" message.
HTTP/1.1 304 Not Modified Connection: close Date: Fri, 11 Mar 2011 12:21:55 GMT Server: Microsoft-IIS/6.0 X-Powered-By: ASP.NET X-AspNet-Version: 2.0.50727 Cache-Control: public Last-Modified: Tue, 05 Jun 2007 15:19:48 GMT
The Solution
So the first thing missing was the "Last Modified" entry in our first response. We added code to include this property.
context.Response.Cache.SetLastModified(resource.LastModifiedDate);
By adding the "Last Modified" date the browser added a new entry to the second request of the image:
If-Modified-Since: Tue, 05 Jun 2007 15:19:48 GMT
But the response was still the same "200 OK" with the complete image. As it turned out you need to handle the "If-Modified-Since" yourself. We added the following code to handle this.
string rawIfModifiedSince = context.Request.Headers.Get("If-Modified-Since"); if (string.IsNullOrEmpty(rawIfModifiedSince)) { // Set Last Modified time context.Response.Cache.SetLastModified(res.LastModifiedDate); } else { DateTime ifModifiedSince = DateTime.Parse(rawIfModifiedSince); if (resource.LastModifiedDate == ifModifiedSince) { // The requested file has not changed context.Response.StatusCode = 304; return; } }
After testing this again the image was still transmitted every time it was requested. A quick debug of the date compare revealed that the HTTP request date time does not contain milliseconds. The following fix was applied.
if (resource.LastModifiedDate.AddMilliseconds(
-resource.LastModifiedDate.Millisecond) == ifModifiedSince)
Now every following request returned a "304 Not Modified" and saves us a lot of traffic and loading time!
Summary
To conclude this post I give you the complete code:
using System; public class ResourceHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetMaxAge(new TimeSpan(1, 0, 0)); string imageName = "some path" Resource resource = CmsHttpContext.Current.RootResourceGallery .GetByRelativePath(imageName) as Resource; if (resource == null) { // Resource not found context.Response.StatusCode = 404; return; } string rawIfModifiedSince = context.Request.Headers .Get("If-Modified-Since"); if (string.IsNullOrEmpty(rawIfModifiedSince)) { // Set Last Modified time context.Response.Cache.SetLastModified(resource.LastModifiedDate); } else { DateTime ifModifiedSince = DateTime.Parse(rawIfModifiedSince); // HTTP does not provide milliseconds, so remove it from the comparison if (resource.LastModifiedDate.AddMilliseconds( -resource.LastModifiedDate.Millisecond) == ifModifiedSince) { // The requested file has not changed context.Response.StatusCode = 304; return; } } using (Stream stream = resource.OpenReadStream()) { byte[] buffer = new byte[32]; while (stream.Read(buffer, 0, 32) > 0) { context.Response.BinaryWrite(buffer); } } } public bool IsReusable { get { return true; } } }
With SharePoint it’s easy to configure multiple zones for your SharePoint Web Application. For example you have a Publishing Web Site with two zones.
- The authenticated CMS where editors can mange content: http://cms.int
- The anonymous website where everybody can view the content: http://www.ext
When the editors link to sites, pages, documents and images the URL will start with http://cms.int. After the content is published it’ll also be available on the anonymous site. Now most of the URLs will be automatically translated to corresponding zone URL and start with http://www.ext.
There are however some place this is not the case. You could try to use relative URLs but even that won’t fix every scenario.
Translate the URL using code
Facing this issue I had to translate the URLs myself. But I wanted to write minimal code. Lucky Microsoft has done most of the work for me.
On the SPFarm Class you will find the AlternateUrlCollections Property. This “collection” is actually an instance of the SPAlternateUrlCollectionManager Class and provides the RebaseUriWithAlternateUri Method. And this is where the magic happens.
This method has an overload where you supply a Uri and a SPUrlZone. You can provide one of the values of the SPUrlZone Enumeration or you can provide the current zone.
To get your current zone you can use the static Lookup Method of the SPAlternateUrl Class. This method requires a Uri so we provide the current one using the ContextUri Property from the same class.
To wrap it all up I give you the code:
var originalUri = new Uri("http://cms.int/pages/default.aspx"); var zone = SPAlternateUrl.Lookup(SPAlternateUrl.ContextUri).UrlZone; var translateUri = SPFarm.Local.AlternateUrlCollections .RebaseUriWithAlternateUri(originalUri, zone)); // When accessing from the authenticated zone // translateUri == "http://cms.int/pages/default.aspx" // When accessing from the anonymous zone // translateUri == http://www.ext/pages/default.aspx
“Other” URLs
If you pass a URL which is not listed as an Alternate Access Mapping the method will return the original URL.
In a previous post I have written about Using the people picker over a one-way trust. In that post I use STSADM commands as there are no other ways to configure this. A downside of the STSADM command is your domain password being visible on the command prompt in plain text for everybody to read.
With SharePoint 2010 Microsoft introduces several cmdlets to replace the “old” STSADM commands. But looking at the STSADM to Windows PowerShell mapping you will see the commands for configuring the people picker are not present.
Creating my own script
PowerShell contains a nice cmdlet called Get-Credential which uses a popup to request credentials from the user and stores the password in a SecureString. This triggered me to write a PowerShell script which will work the same as “STSADM -o setproperty -pn peoplepicker-searchadforests”, but instead of typing the credentials on the command line it will use the credential dialog for every trusted domain.
As written in my previous post the configuration is done in two steps.
SetAppPassword
First you need to create a secure store for the credentials. This is done by executing the SetAppPassword command on every server in your SharePoint Farm with the same password.
STSADM:
stsadm -o setapppassword -password <password>
PowerShell:
SetAppPassword <password>
function SetAppPassword([String]$password) { $type = [Microsoft.SharePoint.Utilities.SPPropertyBag].Assembly .GetType("Microsoft.SharePoint.Utilities.SPSecureString") $method = $type.GetMethod("FromString", "Static, NonPublic", $null, @([String]), $null) $secureString = $method.Invoke($null, @($password)) [Microsoft.SharePoint.SPSecurity]::SetApplicationCredentialKey($secureString) }
PeoplePickerSearchADForests
The second step is to register the (trusted) domains to be visible in the people picker. Remember the setting is per web application and zone.
STSADM:
stsadm -o setproperty -url <url> -pn “peoplepicker-searchadforests” -pv “forest:<source forest>;domain:<trusted domain>,<trusted domain>\<account>,<password>“
PowerShell:
PeoplePickerSearchADForests <url> “forest:<source forest>;domain:<trusted domain>“
function PeoplePickerSearchADForests( [String]$webApplicationUrl, [String]$value) { $webApplication = Get-SPWebApplication $webApplicationUrl $searchActiveDirectoryDomains = $webApplication.PeoplePickerSettings .SearchActiveDirectoryDomains $searchActiveDirectoryDomains.Clear() $currentDomain = (Get-WmiObject -Class Win32_ComputerSystem).Domain if (![String]::IsNullOrEmpty($value)) { $value.Split(@(';'), "RemoveEmptyEntries") | ForEach { $strArray = $_.Split(@(';')) $item = New-Object Microsoft.SharePoint.Administration .SPPeoplePickerSearchActiveDirectoryDomain [String]$value = $strArray[0] $index = $value.IndexOf(':'); if ($index -ge 0) { $item.DomainName = $value.Substring($index + 1); } else { $item.DomainName = $value; } if ([System.Globalization.CultureInfo]::InvariantCulture.CompareInfo .IsPrefix($value, "domain:","IgnoreCase")) { $item.IsForest = $false; } else { $item.IsForest = $true; } if ($item.DomainName -ne $currentDomain) { $credentials = $host.ui.PromptForCredential("Foreign domain trust" + " credentials", "Please enter the trust credentials to connect " + "to the " + $item.DomainName + " domain", "", "") $item.LoginName = $credentials.UserName; $item.SetPassword($credentials.Password); } $searchActiveDirectoryDomains.Add($item); } $webApplication.Update() } }
Using the script
I have attached the script so you can use it in any way you want. You can put the commands in you own .ps1 file, or load the script in your current session using the following syntax:
. .\<path to file>\PeoplePickerSearchADForests.ps1
(yes, that’s a dot, then a space, then the path to the script)
With the introduction of LINQ the difference between writing code for accessing a lists of objects in memory and accessing a list of data in an external data source like SQL is vanishing. Combining a in memory with a external list in a single query was not yet possible. With the introduction of .NET Framework 4.0 this has changed.
In this post I want to filter my SQL data using a list of integers I have stored in memory.
Data model
I have created a small data model as illustration. This will be used for the examples. Follow the link on the image below for a larger version. The model is part of the sample code.
IQueryable.Join
The first option you can use is to use the Queryable.Join Method.
var entities = new DemoModelContainer(); var countryIds = Enumerable.Range(1, 3); var query = from i in entities.Items from c in i.Countries join cid in countryIds on c.Id equals cid select i; var items = query.ToList();
When you look at the generated SQL statement (I love IntelliTrace!) you can see a temporary table is created by using the UNION ALL statement.
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[Items] AS [Extent1] INNER JOIN [dbo].[CountryItem] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Items_Id] INNER JOIN ( SELECT [UnionAll1].[C1] AS [C1] FROM ( SELECT 1 AS [C1] FROM (SELECT 1 AS X) AS [SingleRowTable1] UNION ALL SELECT 2 AS [C1] FROM (SELECT 1 AS X) AS [SingleRowTable2] ) AS [UnionAll1] UNION ALL SELECT 3 AS [C1] FROM (SELECT 1 AS X) AS [SingleRowTable3] ) AS [UnionAll2] ON [Extent2].[Countries_Id] = [UnionAll2].[C1]
What happens is that 2 values are combined in a set, this set is extended with the next value, this set is extended with another value, and so on, and so on, until all values are present.
This works nicely, but as you can imagine, the SQL query will grow rapidly when the IEnumerable list gets larger.
When you change:
var countryIds = Enumerable.Range(1, 3);
into:
var countryIds = Enumerable.Range(1, 50);
you will run into a SqlException:
Some part of your SQL statement is nested too deeply. Rewrite the query or break it up into smaller queries.
SQL Server has a hard-coded limit of nesting queries, it doesn’t matter which version or edition of SQL server you use.
IEnumerable Contains
Joining tables is one way of filtering data, an alternative is to use the WHERE statement. This can be done by using the Queryable.Where Method.
var entities = new DemoModelContainer(); var countryIds = Enumerable.Range(1, 3); var query = from i in entities.Items from c in i.Countries where countryIds.Contains(c.Id) select i; var items = query.ToList();
When you look at the generated SQL statement (Still loving IntelliTrace!) you can see now the IN statement is used.
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[Items] AS [Extent1] INNER JOIN [dbo].[CountryItem] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Items_Id] WHERE [Extent2].[Countries_Id] IN (1,2,3)
This SQL query is much smaller and filtering against a larger set will not give you an exception.
This is how the WHERE statement looks when filtering on 50 values:
WHERE [Extent2].[Countries_Id] IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15, 16,17,18,19,20,21,22,23,24,25,26,27, 28,29,30,31,32,33,34,35,36,37,38,39, 40,41,42,43,44,45,46,47,48,49,50)
Conclusion
You can use both methods to filter SQL data with a list from memory. But when choosing for the join method you should be really sure your query will not reach the maximum level of nesting.
I prefer to use Contains method.
Although I used a many-to-many relation in this example, the same applies for a one-to-many scenario.
Sample code
I’ve added a small Visual Studio 2010 project so you can see the SQL statements for yourself.
You will need SQL Server Express 2008 R2 installed on your machine to run the code.
With SharePoint 2010 the amount of databases on your SQL server has grown quite a bit. By default most of these databases have their recovery model set to ‘FULL’. After some time you will discover you’re running out of space.
The problem
Most likely the problem lies with the transaction logs of your databases. With the recovery model set to ‘FULL’ the will keep storing every transaction until you make a backup. Chances are you don’t configure a backup plan for you development environment as most development databases don’t need a backup as your sources will be stored in a source control system.
The (manual) solution
To solve this problem you can change the recovery model of each database by hand. For this you can open SQL Server Management Studio (SMSS), open the properties screen for a database and navigate to the options tab. There you will find the recovery model option.
Database Properties screen with the Recovery model set to ‘Simple’.
Saving this change will empty your transaction log. But it will not shrink the physical file on disk. To shrink this file you can look at the "Shrink" task.
The context menu’s to shrink the size of the log files.
The (automated) solution
Executing this step for every database manually is quite some work. So you want the easy solution.
The following TSQL script will change the recovery model for every database to ‘simple’ and shrinks the database.
USE [master] GO DECLARE @dbname SYSNAME DECLARE @altercmd NVARCHAR(1000) DECLARE @shrinkcmd NVARCHAR(1000) DECLARE [dbcursor] CURSOR FOR SELECT [name] FROM sysdatabases OPEN [dbcursor] FETCH NEXT FROM [dbcursor] INTO @dbname WHILE @@FETCH_STATUS = 0 BEGIN IF (SELECT DATABASEPROPERTYEX(@dbname, 'RECOVERY')) != 'SIMPLE' AND @dbname != 'tempdb' BEGIN SET @altercmd = 'ALTER DATABASE "' + @dbname + '" SET RECOVERY SIMPLE' EXEC (@altercmd) SET @shrinkcmd = 'DBCC SHRINKDATABASE ("' + @dbname + '")' EXEC (@shrinkcmd) PRINT @dbname END FETCH NEXT FROM [dbcursor] INTO @dbname END CLOSE [dbcursor] DEALLOCATE [dbcursor]
When you have a SharePoint farm and you want to use accounts from another domain you need a partial (one-way) or a full (two-way) trust between those domain.
A full trust is not always desirable and there your problem begins. After setting up the one-way trust you can authenticate with an account from the trusted domain, but the SharePoint People Picker doesn’t show any accounts from this domain.
It has been documented by others before, but as I ran into this recently I’ll give my summary how I fixed this.
This solution is the same for WSS 3.0/SharePoint 2007 as SharePoint 2010.
The problem
When using a one-way trust you don’t see any accounts from the other domain in the people picker.
People picker not showing accounts from the other domain.
The reason
This is an example of how you could use a partial trust.
Example of a one-way trust architecture.
You want to allow employees to authenticate in a development farm, but you don’t want to allow any test or service account from the development domain to authenticate in the company domain.
As the application pool account is based in the development domain it doesn’t have the right to query the company domain.
The solution
Using STSADM we can configure which forests and domains are searched for accounts by setting the peoplepicker-searchadforests property. The best part is that we can supply a username and password for a trusted domain.
SharePoint doesn’t allow you to store this username and password in plain text on the server. So you will have to configure a secure store. If you skip this step, configuring the search account for trusted domains will always fail with the following message.
Cannot retrieve the information for application credential key.
To create a credential key you will have to use the following command.
stsadm -o setapppassword -password <password>
This command has to be executed on every server in the farm.
Now you can configure the forests and domains you want to search using the following command.
stsadm -o setproperty -url <web application url> -pn peoplepicker-searchadforests -pv forest:<source forest>;domain:<trusted domain>,<trusted domain>\<account>,<password>
You can combine any number of forests and domains, but you need to specify at least one. You also need to include all forests and domains in one statement because every time you execute this command it will reset the current settings.
Also note this setting is per web application, and even per zone.
People picker showing accounts from the other domain.
With the launch of Visual Studio 2010 this week a lot of people will start upgrading to the new version. After the installation was complete I noticed the Surface project and item templates were not available. In this post I explain how to get the entries in Visual Studio 2010.
First of all you need to realize that the templates are part of the Microsoft Surface SDK. If you have it installed, skip the next paragraph.
INSTALLING the SURFACE SDK
If you are running Windows 7 or a Windows Vista x64, you need to apply the changes described in a previous post: "Installing the Microsoft Surface SDK on Windows 7 x64".
If you don’t have Visual Studio 2008 on your system and want to start with Visual Studio 2010 from scratch some additional steps are needed patching the Surface SDK Installer. For details on the patch process see the earlier referenced post.
Patching the installer
When you’re patching the MSI with the Orca tool remove also the following conditions:
Select the row with "Installed OR (VS2008SPLEVEL AND VS2008CSPROJSUPPORT) OR VCSEXP2008SPLEVEL" and choose "Drop row"
Select the row with "Installed OR (VS2008SPLEVEL AND VS2008SPLEVEL >= "#0") OR (VCSEXP2008SPLEVEL AND VCSEXP2008SPLEVEL >= "#0")" and choose "Drop row"
Select the row with "Installed OR DEXPLORE" and choose "Drop row"
Select the row with "Installed OR VS90DEVENV OR NOT VS2008SPLEVEL" and choose "Drop row"
Select the row with "Installed OR VCSHARP90EXPRESS OR NOT VCSEXP2008SPLEVEL" and choose "Drop row"
Copying the Templates from the SDK
Now that you have installed the Surface SDK the subsequent steps are quite simple.
The following lines are for x64 systems. If you’re running on x86 change "Program Files (x86)" into "Program Files".
xcopy /s "C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\ItemTemplates\CSharp\Surface\v1.0" "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ItemTemplates\CSharp\Surface\1033\"
xcopy /s "C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\ProjectTemplates\CSharp\Surface\v1.0" "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ProjectTemplates\CSharp\Surface\1033\"
cd "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE"
devenv /setup
After this is finished launch Visual Studio 2010.
The "New Project" dialog with the Surface Application templates listed.
(Remember to select .NET Framework 3.5)
The "Add New Item" dialog with the Surface Control templates listed.
Today the Microsoft Virtualization Team announced the availability of the new beta version of the Linux Integration Services for Hyper-V. There are three big changes in this version:
- Virtual machines will be able to use up to 4 virtual CPU’s.
- Virtual machines will be able to synchronize their time with the parent partition.
- Virtual machines will be able to shutdown gracefully from the Hyper-V manager.
In this post I will try the new features.
Linux Integration Services for Hyper-V 2.0
First I got a Virtual machine (VM) installed as described in my previous post “Running CentOS 5.x on Hyper-V“. I used the current released stable version of the Linux Integration Services (LIS): Version 2.0.
On boot time VMBUS displays Version 2.0
My Hyper-V host only has a dual-core CPU. So it’s impossible for me to test the 4 CPU support. I couldn’t find any differences with 2 CPU’s.
Shutdown from Hyper-V Console
With the current version of the LIS when I press the shutdown button I get the following error:
![]()
Hyper-V Console shows the error “Failed to shut down the virtual machine.”
Time synchronization
With the current version of the LIS I had a lot of trouble with the clock of the VM getting out of sync very fast. I did a post to fix this: “Correcting time drift with CentOS on Hyper-V“.
I did not implement the fix on the VM I created for this post to demonstrate the problem:
![]()
The VM gets an offset of multiple seconds within minutes.
Linux Integration Services for Hyper-V 2.1 Beta
To get the beta drivers you need to download them from the Microsoft Connect website. (Look for the Linux Integration Services for Hyper-V project site).
I installed the new drivers in exact the same way as the 2.0.

On boot time VMBUS displays Version 2.1.2
Not only the new version number is displayed, also the new Shutdown and Timesync channels are mentioned!
Shutdown from Hyper-V Console
Pressing the shutdown button now gives a more expected result:
![]()
The VM receives the signal to shutdown and calls /sbin/poweroff
![]()
The VM is gracefully turned off.
Time synchronization
With the new LIS the time is pretty much stable, nothing NTP can handle. There is no need to change the boot command in grub anymore.
![]()
The VM only shows a minimal time difference over the course of minutes.





