diff mbox series

[bug#36404,v4,2/4] gnu: Add machine type for deployment specifications.

Message ID 87y31gs4k5.fsf_-_@sdf.lonestar.org
State Accepted
Headers show
Series Add 'guix deploy'. | expand

Commit Message

Jakob L. Kreuze July 2, 2019, 5:56 p.m. UTC
* gnu/machine.scm: New file.
* gnu/machine/ssh.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
---
 Makefile.am         |   3 +-
 gnu/local.mk        |   5 +-
 gnu/machine.scm     | 118 ++++++++++++++
 gnu/machine/ssh.scm | 363 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 487 insertions(+), 2 deletions(-)
 create mode 100644 gnu/machine.scm
 create mode 100644 gnu/machine/ssh.scm

Comments

Ludovic Courtès July 4, 2019, 9:19 a.m. UTC | #1
Hi Jakob and all!

Apologies for not moving as fast as you do!  :-)

zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:

> +(define (switch-to-system machine)
> +  "Monadic procedure creating a new generation on MACHINE and execute the
> +activation script for the new system configuration."

[...]

> +(define (upgrade-shepherd-services machine)
> +  "Monadic procedure unloading and starting services on the remote as needed
> +to realize the MACHINE's system configuration."

[...]

> +(define (machine-boot-parameters machine)
> +  "Monadic procedure returning a list of 'boot-parameters' for the generations
> +of MACHINE's system profile, ordered from most recent to oldest."

[...]

> +(define (install-bootloader machine)
> +  "Create a bootloader entry for the new system generation on MACHINE, and
> +configure the bootloader to boot that generation by default."

To me the end goal was to move these “effectful” bits into a script,
such that both ‘guix system reconfigure’ and ‘guix deploy’ would only
have to run that script, locally or remotely.  That would avoid
duplicating these somewhat tricky procedures.

Now, perhaps we can start like this, and leave factorization for later?
I just want to make sure we don’t forget about that and let it evolve
into something we have a hard time maintaining.

WDYT?

Thanks,
Ludo’.
Jakob L. Kreuze July 4, 2019, 3:59 p.m. UTC | #2
Hi Ludovic,

Ludovic Courtès <ludo@gnu.org> writes:

> To me the end goal was to move these “effectful” bits into a script,
> such that both ‘guix system reconfigure’ and ‘guix deploy’ would only
> have to run that script, locally or remotely. That would avoid
> duplicating these somewhat tricky procedures.

Ah, that's starting to ring a bell now. I believe you mentioned that
when 'guix deploy' was initially being proposed, but at the time I
didn't quite register that we'd be extracting the behavior in that way.

> Now, perhaps we can start like this, and leave factorization for
> later? I just want to make sure we don’t forget about that and let it
> evolve into something we have a hard time maintaining.
>
> WDYT?

I agree. I'm getting the impression that people don't want this to sit
in review limbo for too long, and in terms of "commit history hygiene,"
I think it would be better to recognize refactoring out the common
behavior as a distinct change.

Thanks!

Regards,
Jakob
Thompson, David July 5, 2019, 1:32 a.m. UTC | #3
On Tue, Jul 2, 2019 at 1:57 PM Jakob L. Kreuze
<zerodaysfordays@sdf.lonestar.org> wrote:
>
> * gnu/machine.scm: New file.
> * gnu/machine/ssh.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.

I'm OK with refactoring the reconfigure code in a future patch set.
This patch looks good to me!

- Dave
Ludovic Courtès July 5, 2019, 8:10 a.m. UTC | #4
Hi,

"Thompson, David" <dthompson2@worcester.edu> skribis:

> On Tue, Jul 2, 2019 at 1:57 PM Jakob L. Kreuze
> <zerodaysfordays@sdf.lonestar.org> wrote:
>>
>> * gnu/machine.scm: New file.
>> * gnu/machine/ssh.scm: New file.
>> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
>
> I'm OK with refactoring the reconfigure code in a future patch set.

OK, sounds good to me!

Thanks for your feedback,
Ludo’.
Ludovic Courtès July 5, 2019, 8:24 a.m. UTC | #5
zerodaysfordays@sdf.lonestar.org (Jakob L. Kreuze) skribis:

> +(define (build-machine machine)
> +  "Monadic procedure that builds the system derivation for MACHINE and returning
> +a list containing the path of the derivation file and the path of the derivation
> +output."
> +  (let ((os (machine-system machine)))
> +    (mlet* %store-monad ((osdrv (operating-system-derivation os))
> +                         (_ ((store-lift build-derivations) (list osdrv))))
> +      (return (list (derivation-file-name osdrv)
> +                    (derivation->output-path osdrv))))))
> +
> +(define (machine-remote-eval machine exp)
> +  "Evaluate EXP, a gexp, on MACHINE. Ensure that all the elements EXP refers to
> +are built and deployed to MACHINE beforehand."
> +  (let ((environment (machine-environment machine)))
> +    ((environment-type-machine-remote-eval environment) machine exp)))
> +
> +(define (deploy-machine machine)
> +  "Monadic procedure transferring the new system's OS closure to the remote
> +MACHINE, activating it on MACHINE and switching MACHINE to the new generation."
> +  (let ((environment (machine-environment machine)))
> +    ((environment-type-deploy-machine environment) machine)))

In the SSH case, ‘deploy-machine’ should roughly translate to:

  (remote-eval #~(switch-to-system #$os) machine)

Thus, ‘build-machine’ is unnecessary: the actual build of OS is
automatically triggered by ‘remote-eval’, either locally or remotely,
depending on #:build-locally?.

So I believe you can remove ‘build-machine’ altogether.

> +        (error "unsupported configuration type"))))

It’s a bit verbose, but I’d suggest using SRFI-34/35 instead, like so:

  (raise (condition
          (&message (message "unsupported machine configuration type"))))

That way, if you also add the file to po/guix/POTFILES.in, i18n will do
its magic.  :-)

Otherwise it looks great to me!

Ludo’.
Jakob L. Kreuze July 5, 2019, 6:53 p.m. UTC | #6
"Thompson, David" <dthompson2@worcester.edu> writes:

> Replace "path" with "file name". Lots of people use them
> interchangeably, but GNU makes a clear distinction between the two
> terms.

Ah, good to know. Updated.

Ludovic Courtès <ludo@gnu.org> writes:

> Please add this file to po/guix/POTFILES.in so it can be subject to
> localization.
>
>> +(define %default-options
>> +  '((system . ,(%current-system))
>> +    (substitutes? . #t)
>> +    (build-hook? . #t)
>> +    (graft? . #t)
>> +    (debug . 0)
>> +    (verbosity . 2)))
>
> ‘verbosity’ should probably be 1 (only ‘guix build’ and ‘guix system
> build’ default to 2.)
>
>> +      (for-each (lambda (machine)
>> +                  (format #t "building ~a... " (machine-display-name machine))
>> +                  (run-with-store store (build-machine machine))
>> +                  (display "done\n"))
>> +                machines)
>> +      (for-each (lambda (machine)
>> +                  (format #t "deploying to ~a... " (machine-display-name machine))
>> +                  (run-with-store store (deploy-machine machine))
>> +                  (display "done\n"))
>> +                machines))))
>
> For i18n purposes and also to get consistent output, please avoid
> ‘format #t’ and instead write:
>
>   (info (G_ "deploying ~a…~%") (machine-display-name machine))
>
> I think you can omit the “done” message.
>
> As a matter of style, it’s clearer IMO to have only one ‘run-with-store’
> call in the whole program.

As in, create a monadic expression with 'mapm' to evaluate the multiple
calls to '(deploy-machine machine)' in sequence, and then pass that to
'run-with-store'?

> In the SSH case, ‘deploy-machine’ should roughly translate to:
> 
>   (remote-eval #~(switch-to-system #$os) machine)
> 
> Thus, ‘build-machine’ is unnecessary: the actual build of OS is
> automatically triggered by ‘remote-eval’, either locally or remotely,
> depending on #:build-locally?.
> 
> So I believe you can remove ‘build-machine’ altogether.

Thanks for pointing that out; I meant to ask about that since it's kinda
vestigial at this point, but wasn't sure if it would be better to have it for
the UI. But I went ahead and removed it, since we already have code for
showing what derivations are going to be built, etc.

> It’s a bit verbose, but I’d suggest using SRFI-34/35 instead, like so:
> 
>   (raise (condition
>           (&message (message "unsupported machine configuration type"))))
> 
> That way, if you also add the file to po/guix/POTFILES.in, i18n will do
> its magic.  :-)

In the end, I generalized the various configuration-related error messages
into a 'maybe-raise-unsupported-configuration-error' that uses
SRFI-35. Hopefully that's alright -- I believe the manual specifies the
behavior enough that one more detailed message is better than two.

> Yay!
>
> You can add a copyright line for you at the top of guix.texi.
>
>> +@section Invoking @code{guix deploy}
>> +
>> +We've already seen @code{operating-system} declarations used to manage a
>> +machine's configuration locally.  Suppose you need to configure multiple
>> +machines, though---perhaps you're managing a service on the web that's
>> +comprised of several servers.  @command{guix deploy} enables you to use those
>> +same @code{operating-system} declarations to manage multiple remote hosts at
>> +once as a logical ``deployment''.
>
> Perhaps add something like:
>
>   @quotation Note
>   The functionality described in this section is still under development
>   and is subject to change.  Get in touch with us on
>   @email{guix-devel@@gnu.org}!
>   @end quotation
>
> That way, if we make a Guix release before this is all stabilized,
> we make sure people have appropriate expectations.  :-)

I like it!

>> +complex deployment may involve, for example, starting virtual machines through
>> +a VPS provider.  In such a case, a different @var{environment} type would be
>      ^^^
> I would write “Virtual Private Server (VPS)”.
>
> I hope the nitpicking level is acceptable, let me know.  I’m really
> excited to see this land in master!

Oh, I appreciate this level of attention to detail. The hardest part of
technical writing for me is having my writing fit in with the writing around
it when contributing to an existing document, so these kinds of comments from
someone more familiar with the manual are great.

Jakob L. Kreuze (4):
  ssh: Add 'identity' keyword to 'open-ssh-session'.
  gnu: Add machine type for deployment specifications.
  Add 'guix deploy'.
  doc: Add section for 'guix deploy'.

 Makefile.am             |   4 +-
 doc/guix.texi           | 114 +++++++++++++
 gnu/local.mk            |   5 +-
 gnu/machine.scm         | 107 ++++++++++++
 gnu/machine/ssh.scm     | 369 ++++++++++++++++++++++++++++++++++++++++
 guix/scripts/deploy.scm |  84 +++++++++
 guix/ssh.scm            |  10 +-
 po/guix/POTFILES.in     |   2 +
 8 files changed, 689 insertions(+), 6 deletions(-)
 create mode 100644 gnu/machine.scm
 create mode 100644 gnu/machine/ssh.scm
 create mode 100644 guix/scripts/deploy.scm
diff mbox series

Patch

diff --git a/Makefile.am b/Makefile.am
index 42307abae..f10c000ea 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -425,7 +425,8 @@  SCM_TESTS =					\
   tests/import-utils.scm			\
   tests/store-database.scm			\
   tests/store-deduplication.scm			\
-  tests/store-roots.scm
+  tests/store-roots.scm				\
+  tests/machine.scm
 
 SH_TESTS =					\
   tests/guix-build.sh				\
diff --git a/gnu/local.mk b/gnu/local.mk
index 81de156cf..0e17af953 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -562,6 +562,9 @@  GNU_SYSTEM_MODULES =				\
   %D%/system/uuid.scm				\
   %D%/system/vm.scm				\
 						\
+  %D%/machine.scm				\
+  %D%/machine/ssh.scm				\
+						\
   %D%/build/accounts.scm			\
   %D%/build/activation.scm			\
   %D%/build/bootloader.scm			\
@@ -627,7 +630,7 @@  INSTALLER_MODULES =                             \
   %D%/installer/newt/user.scm			\
   %D%/installer/newt/utils.scm			\
   %D%/installer/newt/welcome.scm		\
-  %D%/installer/newt/wifi.scm	
+  %D%/installer/newt/wifi.scm
 
 # Always ship the installer modules but compile them only when
 # ENABLE_INSTALLER is true.
diff --git a/gnu/machine.scm b/gnu/machine.scm
new file mode 100644
index 000000000..3dfcab797
--- /dev/null
+++ b/gnu/machine.scm
@@ -0,0 +1,118 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2019 David Thompson <davet@gnu.org>
+;;; Copyright © 2019 Jakob L. Kreuze <zerodaysfordays@sdf.lonestar.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu machine)
+  #:use-module (gnu system)
+  #:use-module (guix derivations)
+  #:use-module (guix monads)
+  #:use-module (guix records)
+  #:use-module (guix store)
+  #:use-module ((guix utils) #:select (source-properties->location))
+  #:export (environment-type
+            environment-type?
+            environment-type-name
+            environment-type-description
+            environment-type-location
+
+            machine
+            machine?
+            this-machine
+
+            machine-system
+            machine-environment
+            machine-configuration
+            machine-display-name
+
+            build-machine
+            deploy-machine
+            machine-remote-eval))
+
+;;; Commentary:
+;;;
+;;; This module provides the types used to declare individual machines in a
+;;; heterogeneous Guix deployment. The interface allows users of specify system
+;;; configurations and the means by which resources should be provisioned on a
+;;; per-host basis.
+;;;
+;;; Code:
+
+
+;;;
+;;; Declarations for resources that can be provisioned.
+;;;
+
+(define-record-type* <environment-type> environment-type
+  make-environment-type
+  environment-type?
+
+  ;; Interface to the environment type's deployment code. Each procedure
+  ;; should take the same arguments as the top-level procedure of this file
+  ;; that shares the same name. For example, 'machine-remote-eval' should be
+  ;; of the form '(machine-remote-eval machine exp)'.
+  (machine-remote-eval environment-type-machine-remote-eval) ; procedure
+  (deploy-machine      environment-type-deploy-machine)      ; procedure
+
+  ;; Metadata.
+  (name        environment-type-name)       ; symbol
+  (description environment-type-description ; string
+               (default #f))
+  (location    environment-type-location    ; <location>
+               (default (and=> (current-source-location)
+                               source-properties->location))
+               (innate)))
+
+
+;;;
+;;; Declarations for machines in a deployment.
+;;;
+
+(define-record-type* <machine> machine
+  make-machine
+  machine?
+  this-machine
+  (system        machine-system)       ; <operating-system>
+  (environment   machine-environment)  ; symbol
+  (configuration machine-configuration ; configuration object
+                 (default #f)))        ; specific to environment
+
+(define (machine-display-name machine)
+  "Return the host-name identifying MACHINE."
+  (operating-system-host-name (machine-system machine)))
+
+(define (build-machine machine)
+  "Monadic procedure that builds the system derivation for MACHINE and returning
+a list containing the path of the derivation file and the path of the derivation
+output."
+  (let ((os (machine-system machine)))
+    (mlet* %store-monad ((osdrv (operating-system-derivation os))
+                         (_ ((store-lift build-derivations) (list osdrv))))
+      (return (list (derivation-file-name osdrv)
+                    (derivation->output-path osdrv))))))
+
+(define (machine-remote-eval machine exp)
+  "Evaluate EXP, a gexp, on MACHINE. Ensure that all the elements EXP refers to
+are built and deployed to MACHINE beforehand."
+  (let ((environment (machine-environment machine)))
+    ((environment-type-machine-remote-eval environment) machine exp)))
+
+(define (deploy-machine machine)
+  "Monadic procedure transferring the new system's OS closure to the remote
+MACHINE, activating it on MACHINE and switching MACHINE to the new generation."
+  (let ((environment (machine-environment machine)))
+    ((environment-type-deploy-machine environment) machine)))
diff --git a/gnu/machine/ssh.scm b/gnu/machine/ssh.scm
new file mode 100644
index 000000000..6ce106bb2
--- /dev/null
+++ b/gnu/machine/ssh.scm
@@ -0,0 +1,363 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2019 Jakob L. Kreuze <zerodaysfordays@sdf.lonestar.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu machine ssh)
+  #:use-module (gnu bootloader)
+  #:use-module (gnu machine)
+  #:autoload   (gnu packages gnupg) (guile-gcrypt)
+  #:use-module (gnu services)
+  #:use-module (gnu services shepherd)
+  #:use-module (gnu system)
+  #:use-module (guix derivations)
+  #:use-module (guix gexp)
+  #:use-module (guix i18n)
+  #:use-module (guix modules)
+  #:use-module (guix monads)
+  #:use-module (guix records)
+  #:use-module (guix remote)
+  #:use-module (guix ssh)
+  #:use-module (guix store)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-19)
+  #:export (managed-host-environment-type
+
+            machine-ssh-configuration
+            machine-ssh-configuration?
+            machine-ssh-configuration
+
+            machine-ssh-configuration-host-name
+            machine-ssh-configuration-port
+            machine-ssh-configuration-user
+            machine-ssh-configuration-session))
+
+;;; Commentary:
+;;;
+;;; This module implements remote evaluation and system deployment for
+;;; machines that are accessable over SSH and have a known host-name. In the
+;;; sense of the broader "machine" interface, we describe the environment for
+;;; such machines as 'managed-host.
+;;;
+;;; Code:
+
+
+;;;
+;;; Parameters for the SSH client.
+;;;
+
+(define-record-type* <machine-ssh-configuration> machine-ssh-configuration
+  make-machine-ssh-configuration
+  machine-ssh-configuration?
+  this-machine-ssh-configuration
+  (host-name machine-ssh-configuration-host-name) ; string
+  (port      machine-ssh-configuration-port       ; integer
+             (default 22))
+  (user      machine-ssh-configuration-user       ; string
+             (default "root"))
+  (identity  machine-ssh-configuration-identity   ; path to a private key
+             (default #f))
+  (session   machine-ssh-configuration-session    ; session
+             (default #f)))
+
+(define (machine-ssh-session machine)
+  "Return the SSH session that was given in MACHINE's configuration, or create
+one from the configuration's parameters if one was not provided."
+  (let ((config (machine-configuration machine)))
+    (if (machine-ssh-configuration? config)
+        (or (machine-ssh-configuration-session config)
+            (let ((host-name (machine-ssh-configuration-host-name config))
+                  (user (machine-ssh-configuration-user config))
+                  (port (machine-ssh-configuration-port config))
+                  (identity (machine-ssh-configuration-identity config)))
+              (open-ssh-session host-name
+                                #:user user
+                                #:port port
+                                #:identity identity)))
+        (error "unsupported configuration type"))))
+
+
+;;;
+;;; Remote evaluation.
+;;;
+
+(define (managed-host-remote-eval machine exp)
+  "Internal implementation of 'machine-remote-eval' for MACHINE instances with
+an environment type of 'managed-host."
+  (maybe-raise-missing-configuration-error machine)
+  (remote-eval exp (machine-ssh-session machine)))
+
+
+;;;
+;;; System deployment.
+;;;
+
+(define (switch-to-system machine)
+  "Monadic procedure creating a new generation on MACHINE and execute the
+activation script for the new system configuration."
+  (define (remote-exp drv script)
+    (with-extensions (list guile-gcrypt)
+      (with-imported-modules (source-module-closure '((guix config)
+                                                      (guix profiles)
+                                                      (guix utils)))
+        #~(begin
+            (use-modules (guix config)
+                         (guix profiles)
+                         (guix utils))
+
+            (define %system-profile
+              (string-append %state-directory "/profiles/system"))
+
+            (let* ((system #$(derivation->output-path drv))
+                   (number (1+ (generation-number %system-profile)))
+                   (generation (generation-file-name %system-profile number)))
+              (switch-symlinks generation system)
+              (switch-symlinks %system-profile generation)
+              ;; The implementation of 'guix system reconfigure' saves the
+              ;; load path and environment here. This is unnecessary here
+              ;; because each invocation of 'remote-eval' runs in a distinct
+              ;; Guile REPL.
+              (setenv "GUIX_NEW_SYSTEM" system)
+              ;; The activation script may write to stdout, which confuses
+              ;; 'remote-eval' when it attempts to read a result from the
+              ;; remote REPL. We work around this by forcing the output to a
+              ;; string.
+              (with-output-to-string
+                (lambda ()
+                  (primitive-load #$script))))))))
+
+  (let* ((os (machine-system machine))
+         (script (operating-system-activation-script os)))
+    (mlet* %store-monad ((drv (operating-system-derivation os)))
+      (machine-remote-eval machine (remote-exp drv script)))))
+
+;; XXX: Currently, this does NOT attempt to restart running services. This is
+;; also the case with 'guix system reconfigure'.
+;;
+;; See <https://issues.guix.info/issue/33508>.
+(define (upgrade-shepherd-services machine)
+  "Monadic procedure unloading and starting services on the remote as needed
+to realize the MACHINE's system configuration."
+  (define target-services
+    ;; Monadic expression evaluating to a list of (name output-path) pairs for
+    ;; all of MACHINE's services.
+    (mapm %store-monad
+          (lambda (service)
+            (mlet %store-monad ((file ((compose lower-object
+                                                shepherd-service-file)
+                                       service)))
+              (return (list (shepherd-service-canonical-name service)
+                            (derivation->output-path file)))))
+          (service-value
+           (fold-services (operating-system-services (machine-system machine))
+                          #:target-type shepherd-root-service-type))))
+
+  (define (remote-exp target-services)
+    (with-imported-modules '((gnu services herd))
+      #~(begin
+          (use-modules (gnu services herd)
+                       (srfi srfi-1))
+
+          (define running
+            (filter live-service-running (current-services)))
+
+          (define (essential? service)
+            ;; Return #t if SERVICE is essential and should not be unloaded
+            ;; under any circumstance.
+            (memq (first (live-service-provision service))
+                  '(root shepherd)))
+
+          (define (obsolete? service)
+            ;; Return #t if SERVICE can be safely unloaded.
+            (and (not (essential? service))
+                 (every (lambda (requirements)
+                          (not (memq (first (live-service-provision service))
+                                     requirements)))
+                        (map live-service-requirement running))))
+
+          (define to-unload
+            (filter obsolete?
+                    (remove (lambda (service)
+                              (memq (first (live-service-provision service))
+                                    (map first '#$target-services)))
+                            running)))
+
+          (define to-start
+            (remove (lambda (service-pair)
+                      (memq (first service-pair)
+                            (map (compose first live-service-provision)
+                                 running)))
+                    '#$target-services))
+
+          ;; Unload obsolete services.
+          (for-each (lambda (service)
+                      (false-if-exception
+                       (unload-service service)))
+                    to-unload)
+
+          ;; Load the service files for any new services and start them.
+          (load-services/safe (map second to-start))
+          (for-each start-service (map first to-start))
+
+          #t)))
+
+  (mlet %store-monad ((target-services target-services))
+    (machine-remote-eval machine (remote-exp target-services))))
+
+(define (machine-boot-parameters machine)
+  "Monadic procedure returning a list of 'boot-parameters' for the generations
+of MACHINE's system profile, ordered from most recent to oldest."
+  (define bootable-kernel-arguments
+    (@@ (gnu system) bootable-kernel-arguments))
+
+  (define remote-exp
+    (with-extensions (list guile-gcrypt)
+      (with-imported-modules (source-module-closure '((guix config)
+                                                      (guix profiles)))
+        #~(begin
+            (use-modules (guix config)
+                         (guix profiles)
+                         (ice-9 textual-ports))
+
+            (define %system-profile
+              (string-append %state-directory "/profiles/system"))
+
+            (define (read-file path)
+              (call-with-input-file path
+                (lambda (port)
+                  (get-string-all port))))
+
+            (map (lambda (generation)
+                   (let* ((system-path (generation-file-name %system-profile
+                                                             generation))
+                          (boot-parameters-path (string-append system-path
+                                                               "/parameters"))
+                          (time (stat:mtime (lstat system-path))))
+                     (list generation
+                           system-path
+                           time
+                           (read-file boot-parameters-path))))
+                 (reverse (generation-numbers %system-profile)))))))
+
+  (mlet* %store-monad ((generations (machine-remote-eval machine remote-exp)))
+    (return
+     (map (lambda (generation)
+            (match generation
+              ((generation system-path time serialized-params)
+               (let* ((params (call-with-input-string serialized-params
+                                read-boot-parameters))
+                      (root (boot-parameters-root-device params))
+                      (label (boot-parameters-label params)))
+                 (boot-parameters
+                  (inherit params)
+                  (label
+                   (string-append label " (#"
+                                  (number->string generation) ", "
+                                  (let ((time (make-time time-utc 0 time)))
+                                    (date->string (time-utc->date time)
+                                                  "~Y-~m-~d ~H:~M"))
+                                  ")"))
+                  (kernel-arguments
+                   (append (bootable-kernel-arguments system-path root)
+                           (boot-parameters-kernel-arguments params))))))))
+          generations))))
+
+(define (install-bootloader machine)
+  "Create a bootloader entry for the new system generation on MACHINE, and
+configure the bootloader to boot that generation by default."
+  (define bootloader-installer-script
+    (@@ (guix scripts system) bootloader-installer-script))
+
+  (define (remote-exp installer bootcfg bootcfg-file)
+    (with-extensions (list guile-gcrypt)
+      (with-imported-modules (source-module-closure '((gnu build install)
+                                                      (guix store)
+                                                      (guix utils)))
+        #~(begin
+            (use-modules (gnu build install)
+                         (guix store)
+                         (guix utils))
+            (let* ((gc-root (string-append "/" %gc-roots-directory "/bootcfg"))
+                   (temp-gc-root (string-append gc-root ".new")))
+
+              (switch-symlinks temp-gc-root gc-root)
+
+              (unless (false-if-exception
+                       (begin
+                         ;; The implementation of 'guix system reconfigure'
+                         ;; saves the load path here. This is unnecessary here
+                         ;; because each invocation of 'remote-eval' runs in a
+                         ;; distinct Guile REPL.
+                         (install-boot-config #$bootcfg #$bootcfg-file "/")
+                         ;; The installation script may write to stdout, which
+                         ;; confuses 'remote-eval' when it attempts to read a
+                         ;; result from the remote REPL. We work around this
+                         ;; by forcing the output to a string.
+                         (with-output-to-string
+                           (lambda ()
+                             (primitive-load #$installer)))))
+                (delete-file temp-gc-root)
+                (error "failed to install bootloader"))
+
+              (rename-file temp-gc-root gc-root)
+              #t)))))
+
+  (mlet* %store-monad ((boot-parameters (machine-boot-parameters machine)))
+    (let* ((os (machine-system machine))
+           (bootloader ((compose bootloader-configuration-bootloader
+                                 operating-system-bootloader)
+                        os))
+           (bootloader-target (bootloader-configuration-target
+                               (operating-system-bootloader os)))
+           (installer (bootloader-installer-script
+                       (bootloader-installer bootloader)
+                       (bootloader-package bootloader)
+                       bootloader-target
+                       "/"))
+           (menu-entries (map boot-parameters->menu-entry boot-parameters))
+           (bootcfg (operating-system-bootcfg os menu-entries))
+           (bootcfg-file (bootloader-configuration-file bootloader)))
+      (machine-remote-eval machine (remote-exp installer bootcfg bootcfg-file)))))
+
+(define (deploy-managed-host machine)
+  "Internal implementation of 'deploy-machine' for MACHINE instances with an
+environment type of 'managed-host."
+  (maybe-raise-missing-configuration-error machine)
+  (mbegin %store-monad
+    (switch-to-system machine)
+    (upgrade-shepherd-services machine)
+    (install-bootloader machine)))
+
+
+;;;
+;;; Environment type.
+;;;
+
+(define managed-host-environment-type
+  (environment-type
+   (machine-remote-eval managed-host-remote-eval)
+   (deploy-machine      deploy-managed-host)
+   (name                'managed-host-environment-type)
+   (description         "Provisioning for machines that are accessable over SSH
+and have a known host-name. This entails little more than maintaining an SSH
+connection to the host.")))
+
+(define (maybe-raise-missing-configuration-error machine)
+  "Raise an error if MACHINE's configuration is #f."
+  (let ((environment (machine-environment machine)))
+    (unless (machine-configuration machine)
+      (error (format #f (G_ "no configuration specified for environment '~a'")
+                     (symbol->string (environment-type-name environment)))))))