Browse Source

Merge intree freezer tempest plugin

* It merges the intree freezer tempest plugin to a the newly
  seperated repo freezer-tempest-plugin

* Moves freezer/tests/integration/common.py to under freezer-tempest-plugin
  as it is consumed in tests/api/base.py .

* Excluding freezer/tests/freezer_tempest_plugin/tests/api/test_version.py
  as it is dependent on freezer project and it should be moved under
  integration tests.

Change-Id: I6967f915758728827e8ddcd1a45a7023904b694e
Chandan Kumar 1 year ago
parent
commit
65bdf3bcf9

+ 0
- 5
README.rst View File

@@ -11,8 +11,3 @@ More information can be found in the freezer developer documentation.
11 11
 * Documentation: http://docs.openstack.org/developer/freezer-tempest-plugin
12 12
 * Source: http://git.openstack.org/cgit/openstack/freezer-tempest-plugin
13 13
 * Bugs: http://bugs.launchpad.net/freezer
14
-
15
-Features
16
---------
17
-
18
-* TODO

+ 257
- 4
freezer_tempest_plugin/README.rst View File

@@ -1,6 +1,259 @@
1
-==============================
2
-Tempest Integration of Freezer
3
-==============================
1
+Freezer Tempest Tests
2
+=====================
4 3
 
5
-This directory contains Tempest tests to cover the Freezer project.
4
+Integration tests in Freezer are implemented using tempest. This document describes  different approaches to run these tests.
6 5
 
6
+Where to start?
7
+
8
+* If you just want to run the tests as quickly as possible, start with `Run tests inside a devstack VM`_.
9
+* If you want to run tests on your local machine (with services running in devstack), start with `Run tests outside a devstack VM`_.
10
+
11
+  Alternatively there is a slightly different version that uses nose as a testrunner: `Run tests outside a devstack VM (alternative instructions using nose)`_.
12
+
13
+* If you want to run tests on your local machine in PyCharm, start with `Run tests in PyCharm`_.
14
+
15
+* If you want to run the tests on Mac OS X, start with `Mac OS X Instructions`_ and continue with one of the options above.
16
+
17
+Setting up a devstack VM
18
+------------------------
19
+
20
+Install devstack with swift and the freezer [1]_ as well as the freezer-api [2]_ plugins by adding the following lines to you `local.conf`:
21
+
22
+::  
23
+
24
+    enable_plugin freezer https://git.openstack.org/openstack/freezer master
25
+    enable_plugin freezer-api https://git.openstack.org/openstack/freezer-api master
26
+    enable_service s-proxy s-object s-container s-account
27
+
28
+.. [1] https://github.com/openstack/freezer/blob/master/devstack/README.rst
29
+.. [2] https://github.com/openstack/freezer-api/blob/master/devstack/README.rst
30
+
31
+Run tests inside a devstack VM
32
+-------------------------------
33
+
34
+#. Create a devstack VM as described in `Setting up a devstack VM`_
35
+
36
+#. Inside your devstack VM, navigate to `/opt/stack/tempest`.
37
+
38
+#. Run `ostestr -r freezer`
39
+
40
+Debugging tests inside a devstack VM
41
+------------------------------------
42
+
43
+Often a devstack VM is used via SSH without graphical interface. Python has multiple command line debuggers. The out-of-the-box pdb works fine but I recommend pudb [3]_ which looks a bit like the old Turbo-Pascal/C IDE. The following steps are necessary to get it running:
44
+
45
+#. Follow the steps in `Run tests inside a devstack VM`_.
46
+
47
+#. Log into the devstack VM
48
+
49
+#. Install pudb:
50
+
51
+   :: 
52
+
53
+     pip install pudb
54
+
55
+#. Open the test file were you want to set the first breakpoint (more breakpoints can be set interactively later) and add the following line
56
+
57
+   ::
58
+
59
+     import pudb;pu.db
60
+
61
+#. Navigate to `/opt/stack/tempest`.
62
+
63
+#. `ostestr` runs tests in parallel which causes issues with debuggers. To work around that you need to run the relevant test directly. E.g.:
64
+
65
+   ::
66
+
67
+     python -m unittest freezer_tempest_plugin.tests.scenario.test_backups.TestFreezerScenario
68
+
69
+#. It should drop you into the debugger!
70
+
71
+.. [3] https://pypi.python.org/pypi/pudb
72
+
73
+Run tests outside a devstack VM
74
+-------------------------------
75
+
76
+This section describes how to run the tests outside of a devstack VM (e.g. in PyCharm) while using services (keystone, swift, ...) inside a VM.
77
+
78
+#. Create a devstack VM as described in `Setting up a devstack VM`_.
79
+
80
+#. Create and activate a virtual environment for Tempest:
81
+   ::
82
+
83
+      virtualenv --no-site-packages tempest-venv
84
+      . tempest-venv/bin/activate
85
+
86
+#. Clone and install the Tempest project into the virtual environment:
87
+   ::
88
+
89
+     git clone https://github.com/openstack/tempest
90
+     pip install tempest/
91
+
92
+#. Clone and install the Freezer project into the virtual environment:
93
+   ::
94
+
95
+     git clone https://github.com/openstack/freezer
96
+     pip install -e freezer/
97
+
98
+#. Clone and install the Freezer API project into the virtual environment:
99
+   ::
100
+
101
+     git clone https://github.com/openstack/freezer-api
102
+     pip install -e freezer-api/
103
+
104
+#. Initialise a Tempest working directory:
105
+   ::
106
+
107
+     mkdir tempest-working
108
+     cd tempest-working
109
+     tempest init .
110
+     
111
+#. Configure `tempest-working/etc/tempest.conf`. The easiest way to do this is to just copy the config from `/opt/stack/tempest/etc/tempest.conf` inside the devstack VM.
112
+
113
+#. Run the freezer test inside the tempest working directory:
114
+   ::
115
+
116
+     cd tempest-working
117
+     ostestr -r freezer
118
+
119
+Run tests outside a devstack VM (alternative instructions using nose)
120
+---------------------------------------------------------------------
121
+
122
+#. Need to make sure that there is a Devstack or other environment for running Keystone and Swift.
123
+
124
+#. Clone the Tempest Repo::
125
+
126
+    run 'git clone https://github.com/openstack/tempest.git'
127
+
128
+#. Create a virtual environment for Tempest. In these instructions, the Tempest virtual environment is ``~/virtualenvs/tempest-freezer``.
129
+
130
+#. Activate the Tempest virtual environment::
131
+
132
+    run 'source ~/virtualenvs/tempest-freezer/bin/activate'
133
+
134
+#. Make sure you have latest pip installed::
135
+
136
+    run 'pip install --upgrade pip'
137
+
138
+#. Install Tempest requirements.txt and test-requirements.txt in the Tempest virtual environment::
139
+
140
+    run 'pip install -r requirements.txt -r test-requirements.txt'
141
+
142
+#. Install Tempest project into the virtual environment in develop mode::
143
+
144
+    run ‘python setup.py develop’
145
+
146
+#. Create logging.conf in Tempest Repo home dir/etc
147
+
148
+    Make a copy of logging.conf.sample as logging.conf
149
+
150
+    In logging configuration
151
+
152
+    You will see this error on Mac OS X
153
+
154
+    socket.error: [Errno 2] No such file or directory
155
+
156
+    To fix this, edit logging.conf
157
+
158
+    Change ‘/dev/log/ to '/var/run/syslog’ in logging.conf
159
+
160
+    see: https://github.com/baremetal/python-backoff/issues/1 for details
161
+
162
+#. Create tempest.conf in Tempest Repo home dir/etc::
163
+
164
+    run 'oslo-config-generator --config-file etc/config-generator.tempest.conf --output-file etc/tempest.conf'
165
+
166
+    Add the following sections to tempest.conf and modify uri and uri_v3 to point to the host where Keystone is running::
167
+
168
+    [identity]
169
+
170
+    username = freezer
171
+    password = secretservice
172
+    tenant_name = service
173
+    domain_name = default
174
+    admin_username = admin
175
+    admin_password = secretadmin
176
+    admin_domain_name = default
177
+    admin_tenant_name = admin
178
+    alt_username = admin
179
+    alt_password = secretadmin
180
+    alt_tenant_name = admin
181
+    use_ssl = False
182
+    auth_version = v3
183
+    uri = http://10.10.10.6:5000/v2.0/
184
+    uri_v3 = http://10.10.10.6:35357/v3/
185
+
186
+    [auth]
187
+
188
+    allow_tenant_isolation = true
189
+    tempest_roles = admin
190
+
191
+
192
+#. Clone freezer Repo::
193
+
194
+    run 'git clone https://github.com/openstack/freezer.git'
195
+
196
+#. Set the virtual environment to the Tempest virtual environment::
197
+
198
+    run 'source ~/virtualenvs/tempest-freezer/bin/activate'
199
+
200
+#. pip install freezer requirements.txt and test-requirements.txt in Tempest virtual environment::
201
+
202
+    run 'pip install -r requirements.txt -r test-requirements.txt'
203
+
204
+#. Install nose in the Temptest virtual environment::
205
+
206
+    run 'pip install nose'
207
+
208
+#. Install freezer project into the Tempest virtual environment in develop mode::
209
+
210
+    run ‘python setup.py develop’
211
+
212
+#. Set project interpreter (pycharm) to Tempest virtual environment.
213
+
214
+#. Create test config (pycharm) using the Tempest virtual environment as python interpreter::
215
+
216
+    Set the environment variable OS_AUTH_URL to the URI where Keystone is running.  For example, OS_AUTH_URL=http://10.10.10.6:5000/v2.0.
217
+    Set the Working Directory to the Tempest home dir. This will allow Tempest to find the etc/tempest.conf file.
218
+
219
+#. Run the tests in the api directory in the freezer_tempest_plugin directory.
220
+
221
+Mac OS X Instructions
222
+---------------------
223
+
224
+For Mac OS X users you will need to install gnu-tar in ``/usr/local/bin`` and make sure that ``/usr/local/bin`` is in the PATH environment variable before any other directories where a different version of tar can be found. Gnu-tar can be installed as ``gtar`` or ``tar``, either name works.
225
+
226
+Also, currently for Mac OS X users, the latest version of gnu-tar (1.29) will not allow ``--unlink-first`` and ``--overwrite`` options to be used together. Also, gnu-tar will complain about the ``--unlink-first`` argument. To get around these limitations, you will need to modify ``tar_builders.py`` and remove the ``--unlink-first`` option from the ``UNIX_TEMPLATE`` variable.
227
+
228
+Run tests in PyCharm
229
+--------------------
230
+
231
+#. Set up the test environment as described in `Run tests outside a devstack VM`_.
232
+
233
+#. Start PyCharm and open a new project pointing to the cloned freezer directory.
234
+
235
+#. Click `File > Settings > Project: freezer > Project Interpreter`.
236
+
237
+#. Click the gear-wheel icon next to `Project Interpreter` and choose `Add Local`.
238
+
239
+#. Navigate to your virtual environment and select the Python interpreter under `bin/python` and confirm with `OK`
240
+
241
+#. In the left pane, navigate to one of the test scripts in `freezer_tempest_plugin/tests/[api or scenario]/*.py`.
242
+
243
+#. Right-click the file and choose `Run 'Unittests in [..]'`
244
+
245
+#. This test run will most likely fail because it is started from the wrong directory. To fix this, open the dropdown box next to the run button in the top-right corner. Choose `Edit Configurations ..`
246
+
247
+#. Point `Working directory:` to your tempest working directory.
248
+
249
+#. Run the test again, this time it should work!
250
+
251
+
252
+
253
+Troubleshooting
254
+---------------
255
+
256
+If tests fail these are good places to check:
257
+
258
+* freezer-api log: `/var/log/apache2/freezer-api.log`
259
+* freezer-agent log: `$HOME/.freezer/freezer.log`

+ 325
- 0
freezer_tempest_plugin/common.py View File

@@ -0,0 +1,325 @@
1
+# Copyright 2015 Hewlett-Packard
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+#
15
+
16
+import distutils.spawn
17
+import hashlib
18
+import itertools
19
+import json
20
+import os
21
+import random
22
+import shutil
23
+import subprocess
24
+import tempfile
25
+import unittest
26
+
27
+import paramiko
28
+
29
+from six.moves import range
30
+
31
+FREEZERC = distutils.spawn.find_executable('freezer-agent')
32
+
33
+
34
+class CommandFailed(Exception):
35
+    def __init__(self, returncode, cmd, output, stderr):
36
+        super(CommandFailed, self).__init__()
37
+        self.returncode = returncode
38
+        self.cmd = cmd
39
+        self.stdout = output
40
+        self.stderr = stderr
41
+
42
+    def __str__(self):
43
+        return ("Command '%s' returned unexpected exit status %d.\n"
44
+                "stdout:\n%s\n"
45
+                "stderr:\n%s" % (self.cmd, self.returncode,
46
+                                 self.stdout, self.stderr))
47
+
48
+
49
+def dict_to_args(d):
50
+    l = [['--' + k.replace('_', '-'), v] for k, v in d.items()]
51
+    return list(itertools.chain.from_iterable(l))
52
+
53
+
54
+def execute_freezerc(dict, must_fail=False, merge_stderr=False):
55
+    """
56
+    :param dict:
57
+    :type dict: dict[str, str]
58
+    :param must_fail:
59
+    :param merge_stderr:
60
+    :return:
61
+    """
62
+    return execute([FREEZERC] + dict_to_args(dict), must_fail=must_fail,
63
+                   merge_stderr=merge_stderr)
64
+
65
+
66
+def execute(args, must_fail=False, merge_stderr=False):
67
+    """
68
+    Executes specified command for the given action.
69
+    :param args:
70
+    :type args: list[str]
71
+    :param must_fail:
72
+    :param merge_stderr:
73
+    :return:
74
+    """
75
+    stdout = subprocess.PIPE
76
+    stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
77
+    proc = subprocess.Popen(args, stdout=stdout, stderr=stderr)
78
+    result, result_err = proc.communicate()
79
+
80
+    if not must_fail and proc.returncode != 0:
81
+        raise CommandFailed(proc.returncode, ' '.join(args), result,
82
+                            result_err)
83
+    if must_fail and proc.returncode == 0:
84
+        raise CommandFailed(proc.returncode, ' '.join(args), result,
85
+                            result_err)
86
+    return result
87
+
88
+
89
+class Temp_Tree(object):
90
+    def __init__(self, suffix='', dir=None, create=True):
91
+        self.create = create
92
+        if create:
93
+            self.path = tempfile.mkdtemp(dir=dir, prefix='__freezer_',
94
+                                         suffix=suffix)
95
+        else:
96
+            self.path = dir
97
+        self.files = []
98
+
99
+    def __enter__(self):
100
+        return self
101
+
102
+    def cleanup(self):
103
+        if self.create and self.path:
104
+            shutil.rmtree(self.path)
105
+
106
+    def __exit__(self, exc_type, exc_val, exc_tb):
107
+        self.cleanup()
108
+
109
+    def add_random_data(self, ndir=5, nfile=5, size=1024):
110
+        """
111
+        add some files containing random data
112
+
113
+        :param ndir: number of dirs to create
114
+        :param nfile: number of files to create in each dir
115
+        :param size: size of files
116
+        :return: None
117
+        """
118
+        def create_file(path):
119
+            abs_pathname = self.create_file_with_random_data(
120
+                dir_path=path, size=size)
121
+            rel_path_name = abs_pathname[len(self.path) + 1:]
122
+            self.files.append(rel_path_name)
123
+
124
+        for _ in range(nfile):
125
+            create_file(self.path)
126
+
127
+        for _ in range(ndir):
128
+            subdir_path = tempfile.mkdtemp(dir=self.path)
129
+            for _ in range(nfile):
130
+                create_file(subdir_path)
131
+
132
+    def create_file_with_random_data(self, dir_path, size=1024):
133
+        handle, abs_pathname = tempfile.mkstemp(dir=dir_path)
134
+        with open(abs_pathname, 'wb') as fd:
135
+            fd.write(os.urandom(size))
136
+        return abs_pathname
137
+
138
+    def modify_random_files(self, count=1):
139
+        indexes = []
140
+        for _ in range(count):
141
+            indexes.append(random.randint(0, len(self.files) - 1))
142
+        for file_index in indexes:
143
+            file_name = self.files[file_index]
144
+            with open(os.path.join(self.path, file_name), 'ab') as fd:
145
+                size_to_add = int(fd.tell() * 0.5)
146
+                fd.write(os.urandom(size_to_add))
147
+
148
+    def delete_random_files(self, count=1):
149
+        indexes = []
150
+        for _ in range(count):
151
+            indexes.append(random.randint(0, len(self.files) - 1))
152
+        for file_index in indexes:
153
+            file_name = self.files[file_index]
154
+            os.unlink(os.path.join(self.path, file_name))
155
+
156
+    def get_file_hash(self, rel_filepath):
157
+        filepath = os.path.join(self.path, rel_filepath)
158
+        if os.path.isfile(filepath):
159
+            return self._filehash(filepath)
160
+        else:
161
+            return ''
162
+
163
+    def _filehash(self, filepath):
164
+        """
165
+        Get GIT style sha1 hash for a file
166
+
167
+        :param filepath: path of file to hash
168
+        :return: hash of the file
169
+        """
170
+        filesize_bytes = os.path.getsize(filepath)
171
+        hash_obj = hashlib.sha1()
172
+        hash_obj.update(("blob %u\0" % filesize_bytes).encode('utf-8'))
173
+        with open(filepath, 'rb') as handle:
174
+            hash_obj.update(handle.read())
175
+        return hash_obj.hexdigest()
176
+
177
+    def get_file_list(self):
178
+        """
179
+        walks the dir tree and creates a list of relative pathnames
180
+        :return: list of relative file paths
181
+        """
182
+        self.files = []
183
+        for root, dirs, files in os.walk(self.path):
184
+            rel_base = root[len(self.path) + 1:]
185
+            self.files.extend([os.path.join(rel_base, x) for x in files])
186
+        return self.files
187
+
188
+    def is_equal(self, other_tree):
189
+        """
190
+        Checks whether two dir tree contain the same files
191
+        It checks the number of files and the hash of each file.
192
+
193
+        NOTE: tox puts .coverage files in the temp folder (?)
194
+
195
+        :param other_tree: dir tree to compare with
196
+        :return: true if the dir trees contain the same files
197
+        """
198
+        lh_files = [x for x in sorted(self.get_file_list())
199
+                    if not x.startswith('.coverage')]
200
+        rh_files = [x for x in sorted(other_tree.get_file_list())
201
+                    if not x.startswith('.coverage')]
202
+        if lh_files != rh_files:
203
+            return False
204
+        for fname in lh_files:
205
+            if os.path.isfile(fname):
206
+                if self.get_file_hash(fname) != \
207
+                        other_tree.get_file_hash(fname):
208
+                    return False
209
+        return True
210
+
211
+
212
+class TestFS(unittest.TestCase):
213
+    """
214
+    Utility class for setting up the tests.
215
+
216
+    Type of tests depends (also) on the environment variables defined.
217
+
218
+    To enable the ssh storage testing, the following environment
219
+    variables need to be defined:
220
+    - FREEZER_TEST_SSH_KEY
221
+    - FREEZER_TEST_SSH_USERNAME
222
+    - FREEZER_TEST_SSH_HOST
223
+    - FREEZER_TEST_CONTAINER
224
+
225
+    To enable the swift storage testing, the following environment
226
+    variables need to be defined:
227
+    - FREEZER_TEST_OS_TENANT_NAME
228
+    - FREEZER_TEST_OS_USERNAME
229
+    - FREEZER_TEST_OS_REGION_NAME
230
+    - FREEZER_TEST_OS_PASSWORD
231
+    - FREEZER_TEST_OS_AUTH_URL
232
+
233
+    Tests involving LVM snapshots are evoided if:
234
+    - user is not root
235
+    - FREEZER_TEST_NO_LVM is set
236
+    """
237
+
238
+    ssh_key = os.environ.get('FREEZER_TEST_SSH_KEY')
239
+    ssh_username = os.environ.get('FREEZER_TEST_SSH_USERNAME')
240
+    ssh_host = os.environ.get('FREEZER_TEST_SSH_HOST')
241
+    container = os.environ.get('FREEZER_TEST_CONTAINER')
242
+    use_ssh = ssh_key and ssh_username and ssh_host and container
243
+
244
+    os_tenant_name = os.environ.get('FREEZER_TEST_OS_TENANT_NAME')
245
+    os_user_name = os.environ.get('FREEZER_TEST_OS_USERNAME')
246
+    os_region = os.environ.get('FREEZER_TEST_OS_REGION_NAME')
247
+    os_password = os.environ.get('FREEZER_TEST_OS_PASSWORD')
248
+    os_auth_url = os.environ.get('FREEZER_TEST_OS_AUTH_URL')
249
+    use_os = (os_tenant_name and os_user_name and os_region and
250
+              os_password and os_auth_url)
251
+    if use_os:
252
+        os.environ['OS_USERNAME'] = os_user_name
253
+        os.environ['OS_TENANT_NAME'] = os_tenant_name
254
+        os.environ['OS_AUTH_URL'] = os_auth_url
255
+        os.environ['OS_PASSWORD'] = os_password
256
+        os.environ['OS_REGION_NAME'] = os_region
257
+        os.environ['OS_TENANT_ID'] = ''
258
+
259
+    openstack_executable = distutils.spawn.find_executable('openstack')
260
+    swift_executable = distutils.spawn.find_executable('swift')
261
+
262
+    use_lvm = (os.getuid() == 0 and 'FREEZER_TEST_NO_LVM' not in os.environ)
263
+    ssh_executable = distutils.spawn.find_executable('ssh')
264
+
265
+    def setUp(self):
266
+        self.source_tree = Temp_Tree()
267
+        self.dest_tree = Temp_Tree()
268
+        if TestFS.use_ssh:
269
+            self.ssh_client = paramiko.SSHClient()
270
+            self.ssh_client.set_missing_host_key_policy(
271
+                paramiko.AutoAddPolicy())
272
+            self.ssh_client.connect(TestFS.ssh_host,
273
+                                    username=TestFS.ssh_username,
274
+                                    key_filename=TestFS.ssh_key)
275
+
276
+    def tearDown(self):
277
+        self.source_tree.cleanup()
278
+        self.dest_tree.cleanup()
279
+
280
+    def assertTreesMatch(self):
281
+        self.assertTrue(self.source_tree.is_equal(self.dest_tree))
282
+
283
+    def assertTreesMatchNot(self):
284
+        self.assertFalse(self.source_tree.is_equal(self.dest_tree))
285
+
286
+    def get_file_list_ssh(self, sub_path=''):
287
+        ftp = self.ssh_client.open_sftp()
288
+        path = '{0}/{1}'.format(self.container, sub_path)
289
+        return ftp.listdir(path)
290
+
291
+    def remove_ssh_directory(self, sub_path=''):
292
+        cmd = 'rm -rf {0}/{1}'.format(self.container, sub_path)
293
+        self.ssh_client.exec_command(cmd)
294
+
295
+    def get_file_list_openstack(self, container):
296
+        if self.openstack_executable:
297
+            json_result = execute([self.openstack_executable, 'object', 'list',
298
+                                   container, '-f', json])
299
+            result = json.loads(json_result)
300
+            return [x['Name'] for x in result]
301
+        if self.swift_executable:
302
+            result = execute([self.swift_executable, 'list', container])
303
+            return result.split()
304
+        raise Exception(
305
+            "Unable to get container list using openstackclient/swiftclient")
306
+
307
+    def remove_swift_container(self, container):
308
+        if self.openstack_executable:
309
+            execute([self.openstack_executable, 'container',
310
+                     'delete', container])
311
+            execute([self.openstack_executable, 'container',
312
+                     'delete', container + '_segments'])
313
+        elif self.swift_executable:
314
+            execute([self.swift_executable, 'delete', container])
315
+            execute([self.swift_executable, 'delete', container + '_segments'])
316
+        return True
317
+
318
+    def do_backup_and_restore_with_check(self, backup_args, restore_args):
319
+        self.source_tree.add_random_data()
320
+        self.assertTreesMatchNot()
321
+        result = execute_freezerc(backup_args)
322
+        self.assertIsNotNone(result)
323
+        result = execute_freezerc(restore_args)
324
+        self.assertIsNotNone(result)
325
+        self.assertTreesMatch()

+ 6
- 0
freezer_tempest_plugin/config.py View File

@@ -13,3 +13,9 @@
13 13
 #    License for the specific language governing permissions and limitations
14 14
 #    under the License.
15 15
 
16
+from oslo_config import cfg
17
+
18
+service_option = cfg.BoolOpt('freezer',
19
+                             default=True,
20
+                             help="Whether or not freezer is expected to be "
21
+                                  "available")

+ 4
- 5
freezer_tempest_plugin/plugin.py View File

@@ -13,13 +13,11 @@
13 13
 #    License for the specific language governing permissions and limitations
14 14
 #    under the License.
15 15
 
16
-
17 16
 import os
18 17
 
19
-from tempest import config
20 18
 from tempest.test_discover import plugins
21 19
 
22
-from freezer_tempest_plugin import config as project_config
20
+from freezer_tempest_plugin import config as freezer_config
23 21
 
24 22
 
25 23
 class FreezerTempestPlugin(plugins.TempestPlugin):
@@ -31,7 +29,8 @@ class FreezerTempestPlugin(plugins.TempestPlugin):
31 29
         return full_test_dir, base_path
32 30
 
33 31
     def register_opts(self, conf):
34
-        pass
32
+        conf.register_opt(freezer_config.service_option,
33
+                          group='service_available')
35 34
 
36 35
     def get_opt_lists(self):
37
-        pass
36
+        return [('service_available', [freezer_config.service_option])]

+ 269
- 0
freezer_tempest_plugin/tests/api/base.py View File

@@ -0,0 +1,269 @@
1
+# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+from datetime import datetime
16
+from datetime import timedelta
17
+import json
18
+import os
19
+import subprocess
20
+
21
+from time import mktime
22
+
23
+from tempest import test
24
+
25
+from freezer_tempest_plugin.common import Temp_Tree
26
+
27
+
28
+def resolve_paths(metadata):
29
+    """Find all paths associated with a particular backup
30
+
31
+    freezer-agent stores all backups in the timestamped sub-directory of the
32
+    first backup created with a particular (name, hostname) pair, so it isn't
33
+    possible to guess the true location of backup data. This function searches
34
+    the backup container to find both the true parent directory and a list of
35
+    all files associated with the given metadata.
36
+
37
+    :param metadata: the metadata associated with the backup to resolve
38
+    :return: a tuple containing the parent directory and a list of associated
39
+             files, or (None, None)
40
+    """
41
+    base_name = '{}_{}'.format(metadata['hostname'], metadata['backup_name'])
42
+    expected_name = '{}_{}'.format(base_name, metadata['time_stamp'])
43
+
44
+    backup_base_path = os.path.join(metadata['container'], base_name)
45
+    for timestamp in os.listdir(backup_base_path):
46
+        timestamp_abs = os.path.join(backup_base_path, timestamp)
47
+
48
+        matching = filter(lambda p: expected_name in p,
49
+                          os.listdir(timestamp_abs))
50
+        if matching:
51
+            return timestamp_abs, matching
52
+
53
+    return None, None
54
+
55
+
56
+def mutate_timestamp(metadata, days_old):
57
+    """Alter all timestamps of an existing backup
58
+
59
+    Since there's no proper way to assign a timestamp to a backup, this method
60
+    takes an existing backup and modifies all associated timestamps to make it
61
+    otherwise indistinguishable from a backup actually created in the past.
62
+
63
+    :param metadata: the metadata associated with the backup to mutate
64
+    :param days_old: the age (i.e. days before now) that should be set
65
+    """
66
+    date = datetime.now() - timedelta(days=days_old)
67
+    old_time_stamp = metadata['time_stamp']
68
+    new_time_stamp = int(mktime(date.timetuple()))
69
+
70
+    parent_dir, files = resolve_paths(metadata)
71
+    if os.path.basename(parent_dir) == str(old_time_stamp):
72
+        # rename the parent dir, but only if it was created for this
73
+        # backup (the dir may contain other backups with different
74
+        # timestamps that we shouldn't touch)
75
+        new_path = os.path.join(os.path.dirname(parent_dir),
76
+                                str(new_time_stamp))
77
+        os.rename(parent_dir, new_path)
78
+        parent_dir = new_path
79
+
80
+    # rename each file associated with the backup, since each filename
81
+    # contains the timestamp as well
82
+    for old_file in files:
83
+        new_file = old_file.replace(str(old_time_stamp), str(new_time_stamp))
84
+        os.rename(os.path.join(parent_dir, old_file),
85
+                  os.path.join(parent_dir, new_file))
86
+
87
+    # update the metadata before saving to keep things consistent
88
+    metadata['time_stamp'] = new_time_stamp
89
+
90
+
91
+def load_metadata(path):
92
+    """Given a metadata path, return a dict containing parsed values.
93
+
94
+    :param path: the path to load
95
+    :return: a metadata dict
96
+    """
97
+    with open(path, 'r') as f:
98
+        return json.load(f)
99
+
100
+
101
+def save_metadata(metadata, path):
102
+    """Write the given metadata object to the provided path.
103
+
104
+    :param metadata: the metadata dict to write
105
+    :param path: the path at which to write the metadata
106
+    """
107
+    with open(path, 'w') as f:
108
+        json.dump(metadata, f)
109
+
110
+
111
+class BaseFreezerTest(test.BaseTestCase):
112
+    credentials = ['primary']
113
+
114
+    def __init__(self, *args, **kwargs):
115
+
116
+        super(BaseFreezerTest, self).__init__(*args, **kwargs)
117
+
118
+    # noinspection PyAttributeOutsideInit
119
+    def setUp(self):
120
+        super(BaseFreezerTest, self).setUp()
121
+
122
+        self.storage = Temp_Tree()
123
+        self.source_trees = []
124
+        self.backup_count = 0
125
+        self.backup_name = 'backup_test'
126
+
127
+        self.get_environ()
128
+
129
+    def tearDown(self):
130
+
131
+        super(BaseFreezerTest, self).tearDown()
132
+
133
+        for tree in self.source_trees:
134
+            tree.cleanup()
135
+
136
+        self.storage.cleanup()
137
+
138
+    @classmethod
139
+    def get_auth_url(cls):
140
+        return cls.os_primary.auth_provider.auth_client.auth_url[:-len(
141
+            '/auth/tokens')]
142
+
143
+    @classmethod
144
+    def setup_clients(cls):
145
+        super(BaseFreezerTest, cls).setup_clients()
146
+        cls.get_environ()
147
+
148
+    @classmethod
149
+    def get_environ(cls):
150
+        os.environ['OS_PASSWORD'] = cls.os_primary.credentials.password
151
+        os.environ['OS_USERNAME'] = cls.os_primary.credentials.username
152
+        os.environ['OS_PROJECT_NAME'] = cls.os_primary.credentials.tenant_name
153
+        os.environ['OS_TENANT_NAME'] = cls.os_primary.credentials.tenant_name
154
+        os.environ['OS_PROJECT_DOMAIN_NAME'] = \
155
+            cls.os_primary.credentials.project_domain_name
156
+        os.environ['OS_USER_DOMAIN_NAME'] = \
157
+            cls.os_primary.credentials.user_domain_name
158
+
159
+        # Allow developers to set OS_AUTH_URL when developing so that
160
+        # Keystone may be on a host other than localhost.
161
+        if 'OS_AUTH_URL' not in os.environ:
162
+            os.environ['OS_AUTH_URL'] = cls.get_auth_url()
163
+
164
+        # Mac OS X uses gtar located in /usr/local/bin
165
+        os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
166
+
167
+        return os.environ
168
+
169
+    def run_subprocess(self, sub_process_args, fail_message):
170
+
171
+        proc = subprocess.Popen(sub_process_args,
172
+                                stdout=subprocess.PIPE,
173
+                                stderr=subprocess.PIPE,
174
+                                env=self.environ, shell=False)
175
+
176
+        out, err = proc.communicate()
177
+
178
+        self.assertEqual(0, proc.returncode,
179
+                         fail_message + " Output: {0}. "
180
+                                        "Error: {1}".format(out, err))
181
+
182
+        self.assertEqual('', err,
183
+                         fail_message + " Output: {0}. "
184
+                                        "Error: {1}".format(out, err))
185
+
186
+    def create_local_backup(self, hostname=None, compression=None,
187
+                            consistency_check=None, incremental=True,
188
+                            always_level=None, restart_always_level=None,
189
+                            max_level=None):
190
+        """Creates a new backup with the given parameters.
191
+
192
+        The backup will immediately be created using a randomly-generated
193
+        source tree on the local filesystem, and will be stored in a random
194
+        temporary directory using the 'local' storage mode. All generated data
195
+        files will be automatically removed during `tearDown()`, though
196
+        implementations are responsible for cleaning up any additional copies
197
+        or restores created via other methods.
198
+
199
+        :param hostname: if set, set `--hostname` to the given value
200
+        :param compression: if set, set `--compression` to the given value
201
+        :param consistency_check: if True, set `--consistency_check`
202
+        :param incremental: if False, set `--no-incremental`
203
+        :param always_level: sets `--always-level` to the given value
204
+        :param restart_always_level: sets `--restart-always-level`
205
+        :param max_level: sets `--max-level` to the given value
206
+        :return: the path to the stored backup metadata
207
+        """
208
+        metadata_path = os.path.join(
209
+            self.storage.path,
210
+            'metadata-{}.json'.format(self.backup_count))
211
+        self.backup_count += 1
212
+
213
+        tree = Temp_Tree()
214
+        tree.add_random_data()
215
+        self.source_trees.append(tree)
216
+
217
+        backup_args = [
218
+            'freezer-agent',
219
+            '--path-to-backup', tree.path,
220
+            '--container', self.storage.path,
221
+            '--backup-name', self.backup_name,
222
+            '--storage', 'local',
223
+            '--metadata-out', metadata_path,
224
+        ]
225
+
226
+        if hostname:
227
+            backup_args += ['--hostname', hostname]
228
+
229
+        if compression:
230
+            backup_args += ['--compression', compression]
231
+
232
+        if consistency_check:
233
+            backup_args += ['--consistency-check']
234
+
235
+        if incremental:
236
+            if always_level is not None:
237
+                backup_args += ['--always-level', str(always_level)]
238
+
239
+            if max_level is not None:
240
+                backup_args += ['--max-level', str(max_level)]
241
+
242
+            if restart_always_level:
243
+                backup_args += ['--restart-always-level',
244
+                                str(restart_always_level)]
245
+        else:
246
+            backup_args += ['--no-incremental', 'NO_INCREMENTAL']
247
+
248
+        self.run_subprocess(backup_args, 'Test backup to local storage.')
249
+
250
+        return metadata_path
251
+
252
+    def create_mutated_backup(self, days_old=30, **kwargs):
253
+        """Create a local backup with a mutated timestamp
254
+
255
+        This creates a new backup using `create_local_backup()`, modifies it
256
+        using `mutate_timestamp()`, and then returns the resulting (loaded)
257
+        metadata dict.
258
+
259
+        :param days_old: the age of the backup to create
260
+        :param kwargs: arguments to pass to `create_local_backup()`
261
+        :return: the loaded metadata
262
+        """
263
+        metadata_path = self.create_local_backup(**kwargs)
264
+
265
+        metadata = load_metadata(metadata_path)
266
+        mutate_timestamp(metadata, days_old)
267
+        save_metadata(metadata, metadata_path)
268
+
269
+        return metadata

+ 188
- 0
freezer_tempest_plugin/tests/api/test_backup_compress.py View File

@@ -0,0 +1,188 @@
1
+# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import json
16
+import os
17
+import subprocess
18
+
19
+from tempest.lib import decorators
20
+
21
+from freezer_tempest_plugin import common
22
+from freezer_tempest_plugin.tests.api import base
23
+
24
+
25
+class TestFreezerCompressGzip(base.BaseFreezerTest):
26
+    def __init__(self, *args, **kwargs):
27
+        super(TestFreezerCompressGzip, self).__init__(*args, **kwargs)
28
+
29
+    # noinspection PyAttributeOutsideInit
30
+    def setUp(self):
31
+        super(TestFreezerCompressGzip, self).setUp()
32
+
33
+        # create a source tree to backup with a few empty files
34
+        # (files must be empty to avoid encoding errors with pure random data)
35
+        self.source_tree = common.Temp_Tree()
36
+        self.source_tree.add_random_data(size=0)
37
+
38
+        self.storage_tree = common.Temp_Tree()
39
+        self.dest_tree = common.Temp_Tree()
40
+
41
+        self.environ = super(TestFreezerCompressGzip, self).get_environ()
42
+
43
+    def tearDown(self):
44
+        super(TestFreezerCompressGzip, self).tearDown()
45
+
46
+        self.source_tree.cleanup()
47
+        self.dest_tree.cleanup()
48
+        self.storage_tree.cleanup()
49
+
50
+    def _backup(self, name, method):
51
+        # perform a normal backup, with gzip specified
52
+        backup_args = ['freezer-agent',
53
+                       '--path-to-backup',
54
+                       self.source_tree.path,
55
+                       '--container',
56
+                       self.storage_tree.path,
57
+                       '--backup-name',
58
+                       name,
59
+                       '--storage',
60
+                       'local',
61
+                       '--compress',
62
+                       method,
63
+                       '--metadata-out',
64
+                       os.path.join(self.storage_tree.path, 'metadata.json')]
65
+
66
+        self.run_subprocess(backup_args, 'Test gzip backup to local storage.')
67
+
68
+    def _restore(self, name, method):
69
+        restore_args = ['freezer-agent',
70
+                        '--action',
71
+                        'restore',
72
+                        '--restore-abs-path',
73
+                        self.dest_tree.path,
74
+                        '--container',
75
+                        self.storage_tree.path,
76
+                        '--backup-name',
77
+                        name,
78
+                        '--storage',
79
+                        'local',
80
+                        '--compress',
81
+                        method]
82
+
83
+        self.run_subprocess(restore_args, 'Test restore from local storage.')
84
+
85
+    def _metadata(self):
86
+        path = os.path.join(self.storage_tree.path, 'metadata.json')
87
+        with open(path, 'r') as f:
88
+            return json.load(f)
89
+
90
+    def _file_get_mimetype(self, metadata):
91
+        """Given some file metadata, find its mimetype using the file command
92
+
93
+        :param metadata: the parsed json file metadata
94
+        :return: the mimetype
95
+        """
96
+        """
97
+        Data is stored like data/tar/localhost_False/1469786264/0_1469786264 so
98
+        we need build the same directory structure.
99
+        data: the directory that holds the backup data
100
+        tar: the engine used to create backup
101
+        localhost: the hostname of the machine where the backup was taken
102
+        False: it should be backup name or False is backup is not provided
103
+        1469786264: timestamp
104
+        0_1469786264: level zero timestamp
105
+        """
106
+        data_file_path = 'data{0}{1}{0}{2}_{3}{0}{4}{0}{5}_{4}{0}data'.format(
107
+            os.path.sep,
108
+            "tar",  # currently we support only tar
109
+            metadata['hostname'],
110
+            metadata['backup_name'],
111
+            metadata['time_stamp'],
112
+            metadata['curr_backup_level']
113
+        )
114
+        data_file_path = os.path.join(self.storage_tree.path,
115
+                                      data_file_path)
116
+        self.assertEqual(True, os.path.exists(data_file_path))
117
+
118
+        # run 'file' in brief mode to only output the values we want
119
+        proc = subprocess.Popen(['file', '-b', '--mime-type', data_file_path],
120
+                                stdout=subprocess.PIPE)
121
+        out, err = proc.communicate()
122
+        self.assertEqual(0, proc.returncode)
123
+
124
+        return out.strip()
125
+
126
+    @decorators.attr(type="gate")
127
+    def test_freezer_backup_compress_gzip(self):
128
+        backup_name = 'freezer-test-backup-gzip-0'
129
+
130
+        self._backup(backup_name, 'gzip')
131
+        self._restore(backup_name, 'gzip')
132
+
133
+        # metadata should show the correct algorithm
134
+        metadata = self._metadata()
135
+        self.assertIn('compression', metadata)
136
+        self.assertEqual('gzip', metadata['compression'])
137
+
138
+        # file utility should detect the correct mimetype
139
+        gizp_mimetypes = ['application/gzip', 'application/x-gzip']
140
+        mimetype = self._file_get_mimetype(metadata)
141
+        self.assertIn(mimetype, gizp_mimetypes)
142
+
143
+        # actual contents should be the same
144
+        diff_args = ['diff', '-r', '-q',
145
+                     self.source_tree.path,
146
+                     self.dest_tree.path]
147
+        self.run_subprocess(diff_args, 'Verify restored copy is identical to '
148
+                                       'original.')
149
+
150
+    @decorators.attr(type="gate")
151
+    def test_freezer_backup_compress_bzip2(self):
152
+        backup_name = 'freezer-test-backup-bzip2-0'
153
+
154
+        self._backup(backup_name, 'bzip2')
155
+        self._restore(backup_name, 'bzip2')
156
+
157
+        metadata = self._metadata()
158
+        self.assertIn('compression', metadata)
159
+        self.assertEqual('bzip2', metadata['compression'])
160
+
161
+        mimetype = self._file_get_mimetype(metadata)
162
+        self.assertEqual('application/x-bzip2', mimetype)
163
+
164
+        diff_args = ['diff', '-r', '-q',
165
+                     self.source_tree.path,
166
+                     self.dest_tree.path]
167
+        self.run_subprocess(diff_args, 'Verify restored copy is identical to '
168
+                                       'original.')
169
+
170
+    @decorators.attr(type="gate")
171
+    def test_freezer_backup_compress_xz(self):
172
+        backup_name = 'freezer-test-backup-xz-0'
173
+
174
+        self._backup(backup_name, 'xz')
175
+        self._restore(backup_name, 'xz')
176
+
177
+        metadata = self._metadata()
178
+        self.assertIn('compression', metadata)
179
+        self.assertEqual('xz', metadata['compression'])
180
+
181
+        mimetype = self._file_get_mimetype(metadata)
182
+        self.assertEqual('application/x-xz', mimetype)
183
+
184
+        diff_args = ['diff', '-r', '-q',
185
+                     self.source_tree.path,
186
+                     self.dest_tree.path]
187
+        self.run_subprocess(diff_args, 'Verify restored copy is identical to '
188
+                                       'original.')

+ 110
- 0
freezer_tempest_plugin/tests/api/test_fs_backup.py View File

@@ -0,0 +1,110 @@
1
+# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import os
16
+import shutil
17
+
18
+from oslo_utils import uuidutils
19
+from tempest.lib import decorators
20
+
21
+from freezer_tempest_plugin.tests.api import base
22
+
23
+
24
+class TestFreezerFSBackup(base.BaseFreezerTest):
25
+    def __init__(self, *args, **kwargs):
26
+        super(TestFreezerFSBackup, self).__init__(*args, **kwargs)
27
+
28
+    def setUp(self):
29
+        super(TestFreezerFSBackup, self).setUp()
30
+
31
+        test_id = uuidutils.generate_uuid(dashed=False)
32
+
33
+        self.backup_source_dir = (
34
+            "/tmp/freezer-test-backup-source/" + test_id
35
+        )
36
+
37
+        self.backup_source_sub_dir = self.backup_source_dir + "/subdir"
38
+
39
+        self.restore_target_dir = (
40
+            "/tmp/freezer-test-backup-restore/" + test_id
41
+        )
42
+
43
+        self.backup_local_storage_dir = (
44
+            "/tmp/freezer-test-backup-local-storage/" + test_id
45
+        )
46
+
47
+        self.freezer_backup_name = 'freezer-test-backup-fs-0'
48
+
49
+        shutil.rmtree(self.backup_source_dir, True)
50
+        os.makedirs(self.backup_source_dir)
51
+        open(self.backup_source_dir + "/a", 'w').close()
52
+        open(self.backup_source_dir + "/b", 'w').close()
53
+        open(self.backup_source_dir + "/c", 'w').close()
54
+
55
+        os.makedirs(self.backup_source_sub_dir)
56
+        open(self.backup_source_sub_dir + "/x", 'w').close()
57
+        open(self.backup_source_sub_dir + "/y", 'w').close()
58
+        open(self.backup_source_sub_dir + "/z", 'w').close()
59
+
60
+        shutil.rmtree(self.restore_target_dir, True)
61
+        os.makedirs(self.restore_target_dir)
62
+
63
+        shutil.rmtree(self.backup_local_storage_dir, True)
64
+        os.makedirs(self.backup_local_storage_dir)
65
+
66
+        self.environ = super(TestFreezerFSBackup, self).get_environ()
67
+
68
+    def tearDown(self):
69
+        super(TestFreezerFSBackup, self).tearDown()
70
+        shutil.rmtree(self.backup_source_dir, True)
71
+        shutil.rmtree(self.restore_target_dir, True)
72
+        shutil.rmtree(self.backup_local_storage_dir)
73
+
74
+    @decorators.attr(type="gate")
75
+    def test_freezer_fs_backup(self):
76
+        backup_args = ['freezer-agent',
77
+                       '--path-to-backup',
78
+                       self.backup_source_dir,
79
+                       '--container',
80
+                       self.backup_local_storage_dir,
81
+                       '--backup-name',
82
+                       self.freezer_backup_name,
83
+                       '--storage',
84
+                       'local']
85
+
86
+        self.run_subprocess(backup_args, "Test backup to local storage.")
87
+
88
+        restore_args = ['freezer-agent',
89
+                        '--action',
90
+                        'restore',
91
+                        '--restore-abs-path',
92
+                        self.restore_target_dir,
93
+                        '--container',
94
+                        self.backup_local_storage_dir,
95
+                        '--backup-name',
96
+                        self.freezer_backup_name,
97
+                        '--storage',
98
+                        'local']
99
+
100
+        self.run_subprocess(restore_args, "Test restore from local storage.")
101
+
102
+        diff_args = ['diff',
103
+                     '-r',
104
+                     '-q',
105
+                     self.backup_source_dir,
106
+                     self.restore_target_dir]
107
+
108
+        self.run_subprocess(diff_args,
109
+                            "Test backup restore from local storage "
110
+                            "diff.")

+ 94
- 0
freezer_tempest_plugin/tests/api/test_metadata_checksum.py View File

@@ -0,0 +1,94 @@
1
+# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+import subprocess
15
+
16
+from tempest.lib import decorators
17
+
18
+from freezer_tempest_plugin import common
19
+from freezer_tempest_plugin.tests.api import base
20
+
21
+
22
+class TestFreezerMetadataChecksum(base.BaseFreezerTest):
23
+    def __init__(self, *args, **kwargs):
24
+        super(TestFreezerMetadataChecksum, self).__init__(*args, **kwargs)
25
+
26
+    # noinspection PyAttributeOutsideInit
27
+    def setUp(self):
28
+        super(TestFreezerMetadataChecksum, self).setUp()
29
+
30
+        self.environ = super(TestFreezerMetadataChecksum, self).get_environ()
31
+        self.dest_tree = common.Temp_Tree()
32
+        self.backup_name = 'backup_checksum_test'
33
+
34
+    def tearDown(self):
35
+        super(TestFreezerMetadataChecksum, self).tearDown()
36
+
37
+        self.dest_tree.cleanup()
38
+
39
+    @decorators.attr(type="gate")
40
+    def test_freezer_fs_backup_valid_checksum(self):
41
+        # perform a normal backup, but enable consistency checks and save the
42
+        # metadata to disk
43
+        metadata_path = self.create_local_backup(consistency_check=True)
44
+
45
+        metadata = base.load_metadata(metadata_path)
46
+
47
+        # load the stored metadata to retrieve the computed checksum
48
+        self.assertIn('consistency_checksum', metadata,
49
+                      'Checksum must exist in stored metadata.')
50
+
51
+        checksum = metadata['consistency_checksum']
52
+        restore_args = ['freezer-agent',
53
+                        '--action', 'restore',
54
+                        '--restore-abs-path', self.dest_tree.path,
55
+                        '--container', metadata['container'],
56
+                        '--backup-name', self.backup_name,
57
+                        '--storage', 'local',
58
+                        '--consistency-checksum', checksum]
59
+
60
+        self.run_subprocess(restore_args,
61
+                            'Test restore from local storage with '
62
+                            'computed checksum.')
63
+
64
+    @decorators.attr(type="gate")
65
+    def test_freezer_fs_backup_bad_checksum(self):
66
+        # as above, but we'll ignore the computed checksum
67
+        metadata_path = self.create_local_backup(consistency_check=True)
68
+        metadata = base.load_metadata(metadata_path)
69
+
70
+        # make a failing sha256 checksum (assuming no added path string)
71
+        bad_checksum = '0' * 64
72
+
73
+        # attempt to restore using the bad checksum
74
+        restore_args = ['freezer-agent',
75
+                        '--action', 'restore',
76
+                        '--restore-abs-path', self.dest_tree.path,
77
+                        '--container', metadata['container'],
78
+                        '--backup-name', self.backup_name,
79
+                        '--storage', 'local',
80
+                        '--consistency-checksum', bad_checksum]
81
+
82
+        process = subprocess.Popen(restore_args,
83
+                                   stdout=subprocess.PIPE,
84
+                                   stderr=subprocess.PIPE,
85
+                                   env=self.environ, shell=False)
86
+        out, err = process.communicate()
87
+
88
+        # make sure the subprocess exist with an error due to checksum mismatch
89
+        message = '{0} Output: {1} Error: {2}'.format(
90
+            'Restore process should fail with checksum error.',
91
+            out, err)
92
+        self.assertEqual(1, process.returncode, message)
93
+        self.assertEqual('', out, message)
94
+        self.assertNotEqual('', err, message)

+ 100
- 0
freezer_tempest_plugin/tests/api/test_swift_backup.py View File

@@ -0,0 +1,100 @@
1
+# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import os
16
+import shutil
17
+
18
+from oslo_utils import uuidutils
19
+from tempest.lib import decorators
20
+
21
+from freezer_tempest_plugin.tests.api import base
22
+
23
+
24
+class TestFreezerSwiftBackup(base.BaseFreezerTest):
25
+    def __init__(self, *args, **kwargs):
26
+        super(TestFreezerSwiftBackup, self).__init__(*args, **kwargs)
27
+
28
+    def setUp(self):
29
+        super(TestFreezerSwiftBackup, self).setUp()
30
+
31
+        test_id = uuidutils.generate_uuid(dashed=False)
32
+
33
+        self.backup_source_dir = (
34
+            "/tmp/freezer-test-backup-source/" + test_id
35
+        )
36
+        self.backup_source_sub_dir = self.backup_source_dir + "/subdir"
37
+
38
+        self.restore_target_dir = (
39
+            "/tmp/freezer-test-backup-restore/" + test_id
40
+        )
41
+
42
+        self.freezer_container_name = 'freezer-test-container-0'
43
+        self.freezer_backup_name = 'freezer-test-backup-swift-0'
44
+
45
+        shutil.rmtree(self.backup_source_dir, True)
46
+        os.makedirs(self.backup_source_dir)
47
+        open(self.backup_source_dir + "/a", 'w').close()
48
+        open(self.backup_source_dir + "/b", 'w').close()
49
+        open(self.backup_source_dir + "/c", 'w').close()
50
+
51
+        os.makedirs(self.backup_source_sub_dir)
52
+        open(self.backup_source_sub_dir + "/x", 'w').close()
53
+        open(self.backup_source_sub_dir + "/y", 'w').close()
54
+        open(self.backup_source_sub_dir + "/z", 'w').close()
55
+
56
+        shutil.rmtree(self.restore_target_dir, True)
57
+        os.makedirs(self.restore_target_dir)
58
+
59
+        self.environ = super(TestFreezerSwiftBackup, self).get_environ()
60
+
61
+    def tearDown(self):
62
+        super(TestFreezerSwiftBackup, self).tearDown()
63
+
64
+        shutil.rmtree(self.backup_source_dir, True)
65
+        shutil.rmtree(self.restore_target_dir, True)
66
+
67
+    @decorators.attr(type="gate")
68
+    def test_freezer_swift_backup(self):
69
+        backup_args = ['freezer-agent',
70
+                       '--path-to-backup',
71
+                       self.backup_source_dir,
72
+                       '--container',
73
+                       self.freezer_container_name,
74
+                       '--backup-name',
75
+                       self.freezer_backup_name]
76
+
77
+        self.run_subprocess(backup_args, "Test backup to swift.")
78
+
79
+        restore_args = ['freezer-agent',
80
+                        '--action',
81
+                        'restore',
82
+                        '--restore-abs-path',
83
+                        self.restore_target_dir,
84
+                        '--container',
85
+                        self.freezer_container_name,
86
+                        '--backup-name',
87
+                        self.freezer_backup_name,
88
+                        '--storage',
89
+                        'swift']
90
+
91
+        self.run_subprocess(restore_args, "Test restore from swift.")
92
+
93
+        diff_args = ['diff',
94
+                     '-r',
95
+                     '-q',
96
+                     self.backup_source_dir,
97
+                     self.restore_target_dir]
98
+
99
+        self.run_subprocess(diff_args,
100
+                            "Test backup to swift and restore diff.")

+ 25
- 0
freezer_tempest_plugin/tests/api/test_tests_running.py View File

@@ -0,0 +1,25 @@
1
+# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+from tempest.lib import decorators
16
+
17
+from freezer_tempest_plugin.tests.api import base
18
+
19
+
20
+class TestFreezerTestsRunning(base.BaseFreezerTest):
21
+
22
+    @decorators.attr(type="gate")
23
+    def test_tests_running(self):
24
+        # See if tempest plugin tests run.
25
+        self.assertEqual(1, 1, 'Tests are running')

+ 305
- 0
freezer_tempest_plugin/tests/scenario/test_backups.py View File

@@ -0,0 +1,305 @@
1
+# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import hashlib
16
+import json
17
+import os
18
+import shutil
19
+import tempfile
20
+import time
21
+
22
+from tempest.lib.cli import base as cli_base
23
+from tempest.lib.cli import output_parser
24
+
25
+from freezer_tempest_plugin.tests.api import base
26
+
27
+JOB_TABLE_RESULT_COLUMN = 3
28
+
29
+
30
+class BaseFreezerCliTest(base.BaseFreezerTest):
31
+    """Base test case class for all Freezer API tests."""
32
+
33
+    credentials = ['primary']
34
+
35
+    @classmethod
36
+    def setup_clients(cls):
37
+        super(BaseFreezerCliTest, cls).setup_clients()
38
+
39
+        cls.cli = CLIClientWithFreezer(
40
+            username=cls.os_primary.credentials.username,
41
+            # fails if the password contains an unescaped $ sign
42
+            password=cls.os_primary.credentials.password.replace('$', '$$'),
43
+            tenant_name=cls.os_primary.credentials.tenant_name,
44
+            uri=cls.get_auth_url(),
45
+            cli_dir='/usr/local/bin'  # devstack default
46
+        )
47
+        cls.cli.cli_dir = ''
48
+
49
+    def delete_job(self, job_id):
50
+        self.cli.freezer_client(action='job-delete', params=job_id)
51
+
52
+    def create_job(self, job_json):
53
+
54
+        with tempfile.NamedTemporaryFile(delete=False) as job_file:
55
+            job_file.write(json.dumps(job_json))
56
+            job_file.flush()
57
+
58
+            output = self.cli.freezer_client(
59
+                action='job-create',
60
+                params='--file {} --client {}'.format(job_file.name,
61
+                                                      job_json['client_id']))
62
+            job_id = output.split()[1]
63
+            expected = 'Job {} created'.format(job_id)
64
+            self.assertEqual(expected, output.strip())
65
+
66
+            self.addCleanup(self.delete_job, job_id)
67
+
68
+            return job_id
69
+
70
+    def find_job_in_job_list(self, job_id):
71
+        job_list = output_parser.table(
72
+            self.cli.freezer_client(action='job-list', params='-C test_node'))
73
+
74
+        for row in job_list['values']:
75
+            if row[0].strip() == job_id.strip():
76
+                return row
77
+
78
+        self.fail('Could not find job: {}'.format(job_id))
79
+
80
+    def wait_for_job_status(self, job_id, timeout=720):
81
+        start = time.time()
82
+
83
+        while True:
84
+            row = self.find_job_in_job_list(job_id)
85
+
86
+            if row[JOB_TABLE_RESULT_COLUMN]:
87
+                return
88
+            elif time.time() - start > timeout:
89
+                self.fail("Status of job '{}' is '{}'."
90
+                          .format(job_id, row[JOB_TABLE_RESULT_COLUMN]))
91
+            else:
92
+                time.sleep(1)
93
+
94
+    def assertJobColumnEqual(self, job_id, column, expected):
95
+        row = self.find_job_in_job_list(job_id)
96
+        self.assertEqual(expected, row[column])
97
+
98
+
99
+class CLIClientWithFreezer(cli_base.CLIClient):
100
+    def freezer_scheduler(self, action, flags='', params='', fail_ok=False,
101
+                          endpoint_type='publicURL', merge_stderr=False):
102
+        """Executes freezer-scheduler command for the given action.
103
+
104
+        :param action: the cli command to run using freezer-scheduler
105
+        :type action: string
106
+        :param flags: any optional cli flags to use
107
+        :type flags: string
108
+        :param params: any optional positional args to use :type params: string
109
+        :param fail_ok: if True an exception is not raised when the
110
+                        cli return code is non-zero
111
+        :type fail_ok: boolean
112
+        :param endpoint_type: the type of endpoint for the service
113
+        :type endpoint_type: string
114
+        :param merge_stderr: if True the stderr buffer is merged into stdout
115
+        :type merge_stderr: boolean
116
+        """
117
+
118
+        flags += ' --os-endpoint-type %s' % endpoint_type
119
+        flags += ' --os-cacert /etc/ssl/certs/ca-certificates.crt'
120
+        flags += ' --os-project-domain-name Default'
121
+        flags += ' --os-user-domain-name Default'
122
+
123
+        return self.cmd_with_auth(
124
+            'freezer-scheduler', action, flags, params, fail_ok, merge_stderr)
125
+
126
+    def freezer_client(self, action, flags='', params='', fail_ok=False,
127
+                       endpoint_type='publicURL', merge_stderr=True):
128
+        flags += ' --os-endpoint-type %s' % endpoint_type
129
+        flags += ' --os-cacert /etc/ssl/certs/ca-certificates.crt'
130
+        flags += ' --os-project-domain-name Default'
131
+        flags += ' --os-user-domain-name Default'
132
+        return self.cmd_with_auth(
133
+            'freezer', action, flags, params, fail_ok, merge_stderr)
134
+
135
+
136
+# This class is just copied from the freezer repo. Depending on where the
137
+# scenario tests end up we may need to refactore this.
138
+class Temp_Tree(object):
139
+    def __init__(self, suffix='', dir=None, create=True):
140
+        self.create = create
141
+        if create:
142
+            self.path = tempfile.mkdtemp(dir=dir, prefix='__freezer_',
143
+                                         suffix=suffix)
144
+        else:
145
+            self.path = dir
146
+        self.files = []
147
+
148
+    def __enter__(self):
149
+        return self
150
+
151
+    def cleanup(self):
152
+        if self.create and self.path:
153
+            shutil.rmtree(self.path)
154
+
155
+    def __exit__(self, exc_type, exc_val, exc_tb):
156
+        self.cleanup()
157
+
158
+    def add_random_data(self, ndir=5, nfile=5, size=1024):
159
+        """
160
+        add some files containing randoma data
161
+
162
+        :param ndir: number of dirs to create
163
+        :param nfile: number of files to create in each dir
164
+        :param size: size of files
165
+        :return: None
166
+        """
167
+        for x in range(ndir):
168
+            subdir_path = tempfile.mkdtemp(dir=self.path)
169
+            for y in range(nfile):
170
+                abs_pathname = self.create_file_with_random_data(
171
+                    dir_path=subdir_path, size=size)
172
+                rel_path_name = abs_pathname[len(self.path) + 1:]
173
+                self.files.append(rel_path_name)
174
+
175
+    def create_file_with_random_data(self, dir_path, size=1024):
176
+        handle, abs_pathname = tempfile.mkstemp(dir=dir_path)
177
+        with open(abs_pathname, 'wb') as fd:
178
+            fd.write(os.urandom(size))
179
+        return abs_pathname
180
+
181
+    def get_file_hash(self, rel_filepath):
182
+        filepath = os.path.join(self.path, rel_filepath)
183
+        if os.path.isfile(filepath):
184
+            return self._filehash(filepath)
185
+        else:
186
+            return ''
187
+
188
+    def _filehash(self, filepath):
189
+        """
190
+        Get GIT style sha1 hash for a file
191
+
192
+        :param filepath: path of file to hash
193
+        :return: hash of the file
194
+        """
195
+        filesize_bytes = os.path.getsize(filepath)
196
+        hash_obj = hashlib.sha1()
197
+        hash_obj.update(("blob %u\0" % filesize_bytes).encode('utf-8'))
198
+        with open(filepath, 'rb') as handle:
199
+            hash_obj.update(handle.read())
200
+        return hash_obj.hexdigest()
201
+
202
+    def get_file_list(self):
203
+        """
204
+        walks the dir tree and creates a list of relative pathnames
205
+        :return: list of relative file paths
206
+        """
207
+        self.files = []
208
+        for root, dirs, files in os.walk(self.path):
209
+            rel_base = root[len(self.path) + 1:]
210
+            self.files.extend([os.path.join(rel_base, x) for x in files])
211
+        return self.files
212
+
213
+    def is_equal(self, other_tree):
214
+        """
215
+        Checks whether two dir tree contain the same files
216
+        It checks the number of files and the hash of each file.
217
+
218
+        NOTE: tox puts .coverage files in the temp folder (?)
219
+
220
+        :param other_tree: dir tree to compare with
221
+        :return: true if the dir trees contain the same files
222
+        """
223
+        lh_files = [x for x in sorted(self.get_file_list())
224
+                    if not x.startswith('.coverage')]
225
+        rh_files = [x for x in sorted(other_tree.get_file_list())
226
+                    if not x.startswith('.coverage')]
227
+        if lh_files != rh_files:
228
+            return False
229
+        for fname in lh_files:
230
+            if os.path.isfile(fname):
231
+                if self.get_file_hash(fname) != \
232
+                        other_tree.get_file_hash(fname):
233
+                    return False
234
+        return True
235
+
236
+
237
+class TestFreezerScenario(BaseFreezerCliTest):
238
+    def setUp(self):
239
+        super(TestFreezerScenario, self).setUp()
240
+        self.source_tree = Temp_Tree()
241
+        self.source_tree.add_random_data()
242
+        self.dest_tree = Temp_Tree()
243
+
244
+        self.cli.freezer_scheduler(action='start',
245
+                                   flags='-c test_node '
246
+                                         '-f /tmp/freezer_tempest_job_dir/')
247
+
248
+    def tearDown(self):
249
+        super(TestFreezerScenario, self).tearDown()
250
+        self.source_tree.cleanup()
251
+        self.dest_tree.cleanup()
252
+
253
+        self.cli.freezer_scheduler(action='stop',
254
+                                   flags='-c test_node '
255
+                                         '-f /tmp/freezer_tempest_job_dir/')
256
+
257
+    def test_simple_backup(self):
258
+        backup_job = {
259
+            "client_id": "test_node",
260
+            "job_actions": [
261
+                {
262
+                    "freezer_action": {
263
+                        "action": "backup",
264
+                        "mode": "fs",
265
+                        "storage": "local",
266
+                        "backup_name": "backup1",
267
+                        "path_to_backup": self.source_tree.path,
268
+                        "container": "/tmp/freezer_test/",
269
+                    },
270
+                    "max_retries": 3,
271
+                    "max_retries_interval": 60
272
+                }
273
+            ],
274
+            "description": "a test backup"
275
+        }
276
+        restore_job = {
277
+            "client_id": "test_node",
278
+            "job_actions": [
279
+                {
280
+                    "freezer_action": {
281
+                        "action": "restore",
282
+                        "storage": "local",
283
+                        "restore_abs_path": self.dest_tree.path,
284
+                        "backup_name": "backup1",
285
+                        "container": "/tmp/freezer_test/",
286
+                    },
287
+                    "max_retries": 3,
288
+                    "max_retries_interval": 60
289
+                }
290
+            ],
291
+            "description": "a test restore"
292
+        }
293
+
294
+        backup_job_id = self.create_job(backup_job)
295
+        self.cli.freezer_client(action='job-start', params=backup_job_id)
296
+        self.wait_for_job_status(backup_job_id)
297
+        self.assertJobColumnEqual(backup_job_id, JOB_TABLE_RESULT_COLUMN,
298
+                                  'success')
299
+
300
+        restore_job_id = self.create_job(restore_job)
301
+        self.wait_for_job_status(restore_job_id)
302
+        self.assertJobColumnEqual(restore_job_id, JOB_TABLE_RESULT_COLUMN,
303
+                                  'success')
304
+
305
+        self.assertTrue(self.source_tree.is_equal(self.dest_tree))

+ 5
- 0
requirements.txt View File

@@ -3,3 +3,8 @@
3 3
 # process, which may cause wedges in the gate later.
4 4
 
5 5
 pbr>=2.0 # Apache-2.0
6
+oslo.utils>=3.31.0 # Apache-2.0
7
+oslo.config>=5.1.0 # Apache-2.0
8
+paramiko>=2.0.0 # LGPLv2.1+
9
+six>=1.10.0 # MIT
10
+tempest>=17.1.0 # Apache-2.0

+ 2
- 2
setup.cfg View File

@@ -1,5 +1,5 @@
1 1
 [metadata]
2
-name = freezer-tempest-plugin
2
+name = freezer_tempest_plugin
3 3
 summary = Tempest plugin for the freezer project.
4 4
 description-file =
5 5
     README.rst
@@ -48,4 +48,4 @@ output_file = freezer_tempest_plugin/locale/freezer_tempest_plugin.pot
48 48
 
49 49
 [entry_points]
50 50
 tempest.test_plugins =
51
-    freezer_tests = freezer_tempest_plugin.plugin:FreezerTempestPlugin
51
+    freezer_tests = freezer_tempest_plugin.plugin:FreezerTempestPlugin

+ 0
- 5
test-requirements.txt View File

@@ -4,13 +4,8 @@
4 4
 
5 5
 hacking>=0.12.0,<0.13 # Apache-2.0
6 6
 
7
-coverage>=4.0,!=4.4 # Apache-2.0
8
-python-subunit>=0.0.18 # Apache-2.0/BSD
9 7
 sphinx!=1.6.1,>=1.5.1 # BSD
10 8
 oslosphinx>=4.7.0 # Apache-2.0
11
-oslotest>=1.10.0 # Apache-2.0
12
-testrepository>=0.0.18  # Apache-2.0/BSD
13
-testtools>=1.4.0 # MIT
14 9
 
15 10
 # releasenotes
16 11
 reno>=1.8.0 # Apache-2.0

+ 3
- 5
tox.ini View File

@@ -32,9 +32,7 @@ commands =
32 32
 commands = oslo_debug_helper {posargs}
33 33
 
34 34
 [flake8]
35
-# E123, E125 skipped as they are invalid PEP-8.
36
-
35
+ignore = H405,H404,H403,H401
37 36
 show-source = True
38
-ignore = E123,E125
39
-builtins = _
40
-exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
37
+enable-extensions = H203,H106
38
+exclude = .venv,.tox,dist,doc,test,*egg,releasenotes

Loading…
Cancel
Save