Merge lp:~maas-maintainers/maas/backup into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Rejected
Rejected by: MAAS Lander
Proposed branch: lp:~maas-maintainers/maas/backup
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 499 lines (+473/-0)
5 files modified
backup/common.py (+142/-0)
backup/maas-backup (+134/-0)
backup/maas-restore (+186/-0)
backup/tests/maas_local_celeryconfig.py (+3/-0)
backup/tests/maas_local_settings.py (+8/-0)
To merge this branch: bzr merge lp:~maas-maintainers/maas/backup
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+197339@code.launchpad.net

Commit message

WIP

To post a comment you must log in.
lp:~maas-maintainers/maas/backup updated
1780. By Raphaël Badin

Couple of obvious fixes.

1781. By Raphaël Badin

One obvious fix.

1782. By Raphaël Badin

Define config variables.

1783. By Raphaël Badin

Fixes from actual testing.

1784. By Gavin Panella

No need for empty modules.

1785. By Gavin Panella

Extract code to name the database backup directory.

1786. By Gavin Panella

Fix typo.

1787. By Gavin Panella

Restore databases.

1788. By Gavin Panella

Fix mistake when deriving db dump dir.

1789. By Gavin Panella

Fix pg_restore command-line options.

1790. By Raphaël Badin

Fix pgdump/restore thanks jtv.

Revision history for this message
MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

1790. By Raphaël Badin

Fix pgdump/restore thanks jtv.

1789. By Gavin Panella

Fix pg_restore command-line options.

1788. By Gavin Panella

Fix mistake when deriving db dump dir.

1787. By Gavin Panella

Restore databases.

1786. By Gavin Panella

Fix typo.

1785. By Gavin Panella

Extract code to name the database backup directory.

1784. By Gavin Panella

No need for empty modules.

1783. By Raphaël Badin

Fixes from actual testing.

1782. By Raphaël Badin

Define config variables.

1781. By Raphaël Badin

One obvious fix.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'backup'
=== added file 'backup/common.py'
--- backup/common.py 1970-01-01 00:00:00 +0000
+++ backup/common.py 2013-12-06 10:37:36 +0000
@@ -0,0 +1,142 @@
1#!/usr/bin/env python2.7
2# -*- mode: python -*-
3# Copyright 2013 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""Common utilities."""
7
8from __future__ import (
9 absolute_import,
10 print_function,
11 unicode_literals,
12 )
13
14str = None
15
16__metaclass__ = type
17__all__ = [
18 "get_database_backup_directory",
19 "import_settings",
20 "logger",
21 "MAAS_CONFIG_DIR",
22 "MAAS_CONFIG_TARBALL",
23 "makedirs",
24 "run_command",
25 "System",
26]
27
28import errno
29import logging
30import os
31from os import path
32from pipes import quote
33from subprocess import (
34 PIPE,
35 Popen,
36 )
37import sys
38
39import apt
40
41
42logging.basicConfig(
43 level=logging.DEBUG,
44 format='%(asctime)s %(levelname)s %(message)s')
45
46logger = logging.getLogger("maas-backup")
47
48# MAAS variables.
49MAAS_CONFIG_DIR = '/etc/maas'
50MAAS_CONFIG_TARBALL = 'etc-maas.tgz'
51
52# Do *NOT* "from ... import ..." these names; they are modified by
53# import_settings().
54django_settings = None
55celery_settings = None
56
57
58def import_settings(test=False):
59 if test:
60 config_dir = path.join(path.dirname(__file__), 'tests')
61 sys.path[:0] = [config_dir]
62 os.environ['DJANGO_SETTINGS_MODULE'] = 'maas_local_settings'
63 os.environ['CELERY_CONFIG_MODULE'] = 'maas_local_celeryconfig'
64 else:
65 sys.path[:0] = [MAAS_CONFIG_DIR, '/usr/share/maas/']
66 os.environ['DJANGO_SETTINGS_MODULE'] = 'maas.settings'
67 os.environ['CELERY_CONFIG_MODULE'] = 'celeryconfig_cluster'
68 # Import Django settings.
69 from django.conf import settings
70 global django_settings
71 django_settings = settings
72 # Import Celery settings.
73 from celery.app import app_or_default
74 global celery_settings
75 celery_settings = app_or_default().conf
76
77
78def run_command(args, input=None, check_call=True, dry_run=False, env=None):
79 """A wrapper to Popen to run commands in the command-line."""
80 if dry_run:
81 cmd = " ".join(quote(arg) for arg in args)
82 logger.info("Running command: %s" % cmd)
83 return
84 process = Popen(
85 args, stdout=PIPE, stderr=PIPE, stdin=PIPE,
86 shell=False, env=env)
87 stdout, stderr = process.communicate(input)
88 retcode = process.returncode
89 if check_call and retcode != 0:
90 cmd = " ".join(quote(arg) for arg in args)
91 raise Exception(
92 "Command '%s' failed (%d):\n%s\n%s" % (
93 cmd, retcode,
94 stdout.decode('ascii'),
95 stderr.decode('ascii')))
96 return retcode, stdout, stderr
97
98
99def makedirs(path):
100 """Similar to os.makedirs but does not error if the directory exists."""
101 try:
102 os.makedirs(path)
103 except OSError as error:
104 if error.errno != errno.EEXIST:
105 raise
106 # Directory already exists.
107
108
109def get_database_backup_directory(database):
110 """Return a relative directory in which a db will or has been backed up.
111
112 :param database: A Django `DATABASES` entry.
113 :type database: `dict`
114 """
115 return "db.%(NAME)s.user=%(USER)s" % database
116
117
118class System:
119 """Encapsulates information about the system.
120
121 For example, what packages are installed, what is the purpose of
122 this system.
123
124 """
125
126 def __init__(self):
127 super(System, self).__init__()
128 self.cache = apt.Cache()
129
130 def is_package_installed(self, package_name):
131 try:
132 package = self.cache[package_name]
133 except KeyError:
134 return False
135 else:
136 return package.is_installed
137
138 def is_region_controller(self):
139 return self.is_package_installed("maas-region-controller")
140
141 def is_cluster_controller(self):
142 return self.is_package_installed("maas-cluster-controller")
0143
=== added file 'backup/maas-backup'
--- backup/maas-backup 1970-01-01 00:00:00 +0000
+++ backup/maas-backup 2013-12-06 10:37:36 +0000
@@ -0,0 +1,134 @@
1#!/usr/bin/env python2.7
2# -*- mode: python -*-
3# Copyright 2013 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""Backup MAAS."""
7
8from __future__ import (
9 absolute_import,
10 print_function,
11 unicode_literals,
12 )
13
14str = None
15
16__metaclass__ = type
17
18import argparse
19import os
20from os import path
21import sys
22
23import common
24from common import (
25 get_database_backup_directory,
26 logger,
27 System,
28 MAAS_CONFIG_TARBALL,
29 MAAS_CONFIG_DIR,
30 run_command,
31 makedirs,
32 )
33from six import text_type
34
35# For development, add the script's directory to sys.path.
36sys.path[:0] = [path.dirname(__file__)]
37
38
39def backup_config_files(target_directory, dry_run=False):
40 dest_file = path.join(target_directory, MAAS_CONFIG_TARBALL)
41 run_command(
42 ['tar', 'zcpf', dest_file, '-C', MAAS_CONFIG_DIR, '.'],
43 dry_run=dry_run)
44
45
46def backup_databases(target_directory, dry_run=False):
47 databases = [
48 database
49 for database in common.django_settings.DATABASES.values()
50 if database["ENGINE"] == "django.db.backends.postgresql_psycopg2"
51 ]
52 for database in databases:
53 backup_to = path.join(
54 target_directory, get_database_backup_directory(database))
55 command = (
56 "pg_dump",
57 "--file", backup_to,
58 "--host", database["HOST"],
59 "--username", database["USER"],
60 database["NAME"],
61 )
62 # The password cannot be passed on the command-line, bit libpq
63 # recognises PGPASSWORD in the environment.
64 env = dict(os.environ, PGPASSWORD=database["PASSWORD"])
65 run_command(command, env=env, dry_run=dry_run)
66
67
68def backup_region(target_directory, dry_run=False):
69 """Backup region controller to `target_directory`."""
70 logger.info("Backing-up region controller to %s.", target_directory)
71 if dry_run:
72 logger.info("Dry-run.")
73 backup_config_files(target_directory, dry_run=dry_run)
74 backup_databases(target_directory, dry_run=dry_run)
75
76
77def backup_leases_file(target_directory, dry_run=False):
78 leases_file = common.celery_settings.DHCP_LEASES_FILE
79 run_command(
80 ['cp', '-p', '--', leases_file, target_directory],
81 check_call=False,
82 dry_run=dry_run)
83
84
85def backup_cluster(target_directory, dry_run=False):
86 """Backup cluster controller to `target_directory`."""
87 logger.info("Backing-up cluster controller to %s.", target_directory)
88 if dry_run:
89 logger.info("Dry-run.")
90 backup_config_files(target_directory, dry_run=dry_run)
91 backup_leases_file(target_directory, dry_run=dry_run)
92
93
94def backup(target_directory, dry_run=False, test=False):
95 """Backup cluster and/or region controller to `target_directory`."""
96 system = System()
97 if test or system.is_region_controller():
98 target_dir = path.join(target_directory, "region")
99 makedirs(target_dir)
100 backup_region(target_dir, dry_run or test)
101 if test or system.is_cluster_controller():
102 target_dir = path.join(target_directory, "cluster")
103 makedirs(target_dir)
104 backup_cluster(target_dir, dry_run or test)
105
106
107# See http://docs.python.org/release/2.7/library/argparse.html.
108argument_parser = argparse.ArgumentParser(description=__doc__)
109argument_parser.add_argument(
110 'target_directory', metavar="target-directory", type=text_type,
111 help="The directory where the files will be put.")
112argument_parser.add_argument(
113 '--dry-run', action='store_true', default=False, help=(
114 "No action; perform a simulation of events that would occur but do "
115 "not actually do anything."))
116argument_parser.add_argument(
117 '--test', action='store_true', default=False, help=(
118 "Run a test run. Equivalent to --dry-run plus skipping all the "
119 "checks."))
120
121
122def main():
123 args = argument_parser.parse_args()
124 if not args.test and os.geteuid() != 0:
125 logger.error(
126 "This script must be run with root privileges ("
127 "i.e. use 'sudo').")
128 return
129 common.import_settings(args.test)
130 backup(args.target_directory, args.dry_run, args.test)
131
132
133if __name__ == "__main__":
134 main()
0135
=== added file 'backup/maas-restore'
--- backup/maas-restore 1970-01-01 00:00:00 +0000
+++ backup/maas-restore 2013-12-06 10:37:36 +0000
@@ -0,0 +1,186 @@
1#!/usr/bin/env python2.7
2# -*- mode: python -*-
3# Copyright 2013 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""Restore MAAS from a backup."""
7
8from __future__ import (
9 absolute_import,
10 print_function,
11 unicode_literals,
12 )
13
14str = None
15
16__metaclass__ = type
17
18import argparse
19import os
20from os import path
21import sys
22from urlparse import urlparse
23
24import common
25from common import (
26 get_database_backup_directory,
27 logger,
28 System,
29 MAAS_CONFIG_TARBALL,
30 MAAS_CONFIG_DIR,
31 run_command,
32 )
33from six import text_type
34
35# For development, add the script's directory to sys.path.
36sys.path[:0] = [path.dirname(__file__)]
37
38
39def restore_config_files(source_directory, dry_run=False):
40 src_file = path.join(source_directory, MAAS_CONFIG_TARBALL)
41 run_command(
42 ['tar', 'zxpf', src_file, '-C', MAAS_CONFIG_DIR],
43 dry_run=dry_run)
44
45
46def restore_databases(source_directory, dry_run=False):
47 databases = [
48 database
49 for database in common.django_settings.DATABASES.values()
50 if database["ENGINE"] == "django.db.backends.postgresql_psycopg2"
51 ]
52 for database in databases:
53 restore_from = path.join(
54 source_directory, get_database_backup_directory(database))
55 command = (
56 "psql",
57 "--host", database["HOST"],
58 "--dbname", database["NAME"],
59 "--username", database["USER"],
60 "--single-transaction",
61 "--file", restore_from,
62 )
63 # The password cannot be passed on the command-line, bit libpq
64 # recognises PGPASSWORD in the environment.
65 env = dict(os.environ, PGPASSWORD=database["PASSWORD"])
66 run_command(command, env=env, dry_run=dry_run)
67
68
69def update_rabbitmq_passwords(dry_run=False, test=False):
70 # Update password for maas_longpoll.
71 run_command(
72 ['rabbitmqctl', 'change_password',
73 common.django_settings.RABBITMQ_USERID,
74 common.django_settings.RABBITMQ_PASSWORD],
75 dry_run=dry_run)
76 # Update password for maas_workers.
77 broker_url = common.celery_settings.BROKER_URL
78 parsed_url = urlparse(broker_url)
79 run_command(
80 ['rabbitmqctl', 'change_password',
81 parsed_url.username, parsed_url.password],
82 dry_run=dry_run)
83
84
85def run_db_migrations(dry_run=False):
86 run_command(
87 ['maas', 'migrate', 'maasserver', '--noinput'],
88 dry_run=dry_run)
89 run_command(
90 ['maas', 'migrate', 'metadataserver', '--noinput'],
91 dry_run=dry_run)
92
93
94def restart_region_services(dry_run=False):
95 run_command(['service', 'apache2', 'restart'], dry_run=dry_run)
96 run_command(['service', 'maas-pserv', 'restart'], dry_run=dry_run)
97 run_command(
98 ['service', 'maas-region-celery', 'restart'], dry_run=dry_run)
99 run_command(['service', 'maas-txlongpoll', 'restart'], dry_run=dry_run)
100 # Do not error if this fails because this MAAS instance might not be
101 # using maas-dns.
102 run_command(
103 ['service', 'bind9', 'restart'], check_call=False, dry_run=dry_run)
104
105
106def restore_region(source_directory, dry_run=False):
107 """Restore region controller from `source_directory`."""
108 logger.info("Restoring region controller from %s.", source_directory)
109 if dry_run:
110 logger.info("Dry-run.")
111 restore_config_files(source_directory, dry_run=dry_run)
112 restore_databases(source_directory, dry_run=dry_run)
113 update_rabbitmq_passwords(dry_run=dry_run)
114 restart_region_services(dry_run=dry_run)
115 run_db_migrations(dry_run=dry_run)
116
117
118def restore_leases_file(source_directory, dry_run=False):
119 leases_file = common.celery_settings.DHCP_LEASES_FILE
120 source_file = path.join(
121 source_directory, path.basename(leases_file))
122 run_command(
123 ['cp', '-p', '--', source_file, leases_file],
124 check_call=False,
125 dry_run=dry_run)
126
127
128def restart_cluser_services(dry_run=False):
129 run_command(
130 ['service', 'maas-cluster-celery', 'restart'], dry_run=dry_run)
131 # Do not error if this fails because this MAAS instance might not be
132 # using maas-dhcp.
133 run_command(
134 ['service', 'maas-dhcp-server', 'restart'], check_call=False,
135 dry_run=dry_run)
136
137
138def restore_cluster(source_directory, dry_run=False):
139 """Restore cluster controller from `source_directory`."""
140 logger.info("Restoring cluster controller from %s.", source_directory)
141 if dry_run:
142 logger.info("Dry-run.")
143 restore_config_files(source_directory, dry_run=dry_run)
144 restore_leases_file(source_directory, dry_run=dry_run)
145 restart_cluser_services(dry_run=dry_run)
146
147
148def restore(source_directory, dry_run=False, test=False):
149 """Restore cluster and/or region controller from `source_directory`."""
150 system = System()
151 if test or system.is_region_controller():
152 source_dir = path.join(source_directory, "region")
153 restore_region(source_dir, dry_run or test)
154 if test or system.is_cluster_controller():
155 source_dir = path.join(source_directory, "cluster")
156 restore_cluster(source_dir, dry_run or test)
157
158
159# See http://docs.python.org/release/2.7/library/argparse.html.
160argument_parser = argparse.ArgumentParser(description=__doc__)
161argument_parser.add_argument(
162 'source_directory', metavar="source-directory", type=text_type,
163 help="The directory where the files from a backup can be found.")
164argument_parser.add_argument(
165 '--dry-run', action='store_true', default=False, help=(
166 "No action; perform a simulation of events that would occur but do "
167 "not actually do anything."))
168argument_parser.add_argument(
169 '--test', action='store_true', default=False, help=(
170 "Run a test run. Equivalent to --dry-run plus skipping all the "
171 "checks."))
172
173
174def main():
175 args = argument_parser.parse_args()
176 if not args.test and os.geteuid() != 0:
177 logger.error(
178 "This script must be run with root privileges ("
179 "i.e. use 'sudo').")
180 return
181 common.import_settings(args.test)
182 restore(args.source_directory, args.dry_run, args.test)
183
184
185if __name__ == "__main__":
186 main()
0187
=== added directory 'backup/tests'
=== added file 'backup/tests/maas_local_celeryconfig.py'
--- backup/tests/maas_local_celeryconfig.py 1970-01-01 00:00:00 +0000
+++ backup/tests/maas_local_celeryconfig.py 2013-12-06 10:37:36 +0000
@@ -0,0 +1,3 @@
1BROKER_URL = 'amqp://maas_workers:TqgnPD6jXe7GT2nVFDJc@10.55.61.76:5672//maas_workers'
2
3DHCP_LEASES_FILE = '/var/lib/maas/dhcp/dhcpd.leases'
04
=== added file 'backup/tests/maas_local_settings.py'
--- backup/tests/maas_local_settings.py 1970-01-01 00:00:00 +0000
+++ backup/tests/maas_local_settings.py 2013-12-06 10:37:36 +0000
@@ -0,0 +1,8 @@
1# RabbitMQ settings.
2RABBITMQ_HOST = 'localhost'
3RABBITMQ_USERID = 'maas_longpoll'
4RABBITMQ_PASSWORD = 'gQaFNcMkzoSloOxHY7ZN'
5RABBITMQ_VIRTUAL_HOST = '/maas_longpoll'
6
7# Settings set so that Django accepts this file as a valid config file.
8SECRET_KEY = '546908346890'