Initial commit of Glazier project code.

This commit is contained in:
Matt LaPlante
2017-01-23 10:57:59 -05:00
parent a637d1e53b
commit 0b086ece5e
100 changed files with 9075 additions and 1 deletions

25
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,25 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to any Google project must be accompanied by a Contributor License
Agreement. This is necessary because you own the copyright to your changes, even
after your contribution becomes part of this project. So this agreement simply
gives us permission to use and redistribute your contributions as part of the
project. Head over to <https://cla.developers.google.com/> to see your current
agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult [GitHub Help] for more
information on using pull requests.
[GitHub Help]: https://help.github.com/articles/about-pull-requests/

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1 +1,40 @@
# glazier # Glazier
Glazier is a tool for automating the installation of the Microsoft Windows
operating system on various device platforms.
[TOC]
## Why Glazier?
Glazier was created with certain principles in mind.
__Text-based & Code-driven__
With Glazier, imaging is configured entirely via text files. This allows
technicians to leverage source control systems to maintain and develop their
imaging platform. By keeping imaging configs in source control, we gain peer
review, change history, rollback/forward, and all the other benefits normally
reserved for writing code.
Reuse and templating allows for config sharing across multiple image types.
Configs can be consumed by unit tests, build simulators, and other helper
infrastructure to build a robust, automated imaging pipeline.
Source controlled text makes it easy to integrate configs across multiple
branches, making it easy to QA new changes before releasing them to the general
population.
__Scalability__
Glazier distributes all data over HTTPS, which means you can use as simple or as
advanced of a distribution platform as you need. Run it from a simple free web
server or a large cloud-based CDN.
Proxies make it easy to accelerate image deployment to remote sites.
__Extensible__
Glazier makes it simple to extend the installer by writing a bit of Python or
Powershell code.

14
__init__.py Normal file
View File

@@ -0,0 +1,14 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

90
autobuild.py Normal file
View File

@@ -0,0 +1,90 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Windows installation and configuration tool."""
import os
import sys
from glazier.lib import buildinfo
from glazier.lib import constants
from glazier.lib import logs
from glazier.lib.config import builder
from glazier.lib.config import runner
import gflags as flags
FLAGS = flags.FLAGS
flags.DEFINE_bool('preserve_tasks', False,
'Preserve the existing task list, if any.')
logging = logs.logging
_FAILURE_MSG = ('%s\n\nInstaller cannot continue.')
class AutoBuild(object):
"""The AutoBuild class manages the imaging process."""
def __init__(self):
logs.Setup()
self._build_info = buildinfo.BuildInfo()
def _LogFatal(self, msg):
"""Log a fatal error and exit.
Args:
msg: The error message to accompany the failure.
"""
logging.fatal(_FAILURE_MSG, msg)
sys.exit(1)
def _SetupTaskList(self):
"""Determines the location of the task list and erases if necessary."""
location = constants.WINPE_TASK_LIST
if FLAGS.environment == 'Host':
location = constants.SYS_TASK_LIST
logging.debug('Using task list at %s', location)
if not FLAGS.preserve_tasks and os.path.exists(location):
logging.debug('Purging old task list.')
try:
os.remove(location)
except OSError as e:
self._LogFatal('Unable to remove task list. %s' % e)
return location
def RunBuild(self):
"""Perform the build."""
task_list = self._SetupTaskList()
if not os.path.exists(task_list):
root_path = FLAGS.config_root_path or '/'
try:
b = builder.ConfigBuilder(self._build_info)
b.Start(out_file=task_list, in_path=root_path)
except builder.ConfigBuilderError as e:
self._LogFatal(str(e))
try:
r = runner.ConfigRunner(self._build_info)
r.Start(task_list=task_list)
except runner.ConfigRunnerError as e:
self._LogFatal(str(e))
def main(unused_argv):
ab = AutoBuild()
ab.RunBuild()
if __name__ == '__main__':
main(sys.argv)

71
autobuild_test.py Normal file
View File

@@ -0,0 +1,71 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for autobuild."""
from fakefs import fake_filesystem
from glazier import autobuild
import mock
import unittest
class LogFatal(Exception):
pass
class BuildInfoTest(unittest.TestCase):
@mock.patch.object(autobuild, 'logs', autospec=True)
def setUp(self, logs):
self.autobuild = autobuild.AutoBuild()
autobuild.logging = logs.logging
autobuild.logging.fatal.side_effect = LogFatal()
def testLogFatal(self):
self.assertRaises(LogFatal, self.autobuild._LogFatal,
'failure is always an option')
self.assertTrue(autobuild.logging.fatal.called)
def testSetupTaskList(self):
cache = autobuild.constants.SYS_CACHE
filesystem = fake_filesystem.FakeFilesystem()
filesystem.CreateFile(r'X:\task_list.yaml')
autobuild.os = fake_filesystem.FakeOsModule(filesystem)
self.assertEqual(self.autobuild._SetupTaskList(),
'%s\\task_list.yaml' % cache)
autobuild.FLAGS.preserve_tasks = True
self.assertEqual(self.autobuild._SetupTaskList(),
'%s\\task_list.yaml' % cache)
autobuild.FLAGS.environment = 'WinPE'
self.assertEqual(self.autobuild._SetupTaskList(), r'X:\task_list.yaml')
self.assertTrue(autobuild.os.path.exists(r'X:\task_list.yaml'))
autobuild.FLAGS.preserve_tasks = False
self.assertEqual(self.autobuild._SetupTaskList(), r'X:\task_list.yaml')
self.assertFalse(autobuild.os.path.exists(r'X:\task_list.yaml'))
@mock.patch.object(autobuild.runner, 'ConfigRunner', autospec=True)
@mock.patch.object(autobuild.builder, 'ConfigBuilder', autospec=True)
def testRunBuild(self, builder, runner):
self.autobuild.RunBuild()
# ConfigBuilderError
builder.side_effect = autobuild.builder.ConfigBuilderError
self.assertRaises(LogFatal, self.autobuild.RunBuild)
# ConfigRunnerError
builder.side_effect = None
runner.side_effect = autobuild.runner.ConfigRunnerError
self.assertRaises(LogFatal, self.autobuild.RunBuild)
if __name__ == '__main__':
unittest.main()

13
chooser/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

136
chooser/chooser.py Normal file
View File

@@ -0,0 +1,136 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""UI for obtaining dynamic configuration settings from the user.
The chooser takes an option file in yaml format which specifies options to
be offered to the user. The UI is populated dynamically with the options and
various types of form inputs.
The UI automatically times out to prevent it from blocking the build. If the
user interacts with the UI before the timer expires, the countdown stops and
the user must click to resume the build. Fields are assigned default values
at startup, and these defaults are also the final values if the UI times out.
When the UI exits, the final state of all the forms is written to the answer
file, to be consumed by the caller.
"""
import logging
from glazier.chooser import fields
from glazier.lib import resources
import Tkinter as tk
class Chooser(object):
"""Dynamic UI for user configuration."""
def __init__(self, options, preload=True):
self.fields = {}
self.responses = {}
self.root = tk.Tk()
self.row = 0
if preload:
self._GuiHeader()
self._LoadOptions(options)
self._GuiFooter()
def _AddExpander(self):
"""Adds an empty Frame which expands vertically in the UI."""
expander = tk.Frame(self.root)
expander.grid(column=0, row=self.row)
self.root.rowconfigure(self.row, weight=1)
self.row += 1
def _AddSeparator(self):
"""Adds a Separator visual element (UI decoration)."""
sep = fields.Separator(self.root)
sep.grid(column=0, row=self.row, sticky='EW')
self.root.rowconfigure(self.row, weight=0)
self.row += 1
def Display(self):
"""Displays the UI on screen."""
if self.fields:
w, h = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
self.root.geometry('%dx%d+0+0' % (w, h))
self.root.focus_set()
self.timer.Countdown()
self.root.mainloop()
self._Quit()
def _GuiFooter(self):
"""Creates all UI elements below the input fields."""
self._AddExpander()
self.timer = fields.Timer(self.root)
self.timer.grid(column=0, row=self.row)
self.root.bind('<Key>', self.timer.Pause)
self.root.bind('<Button-1>', self.timer.Pause)
self.row += 1
self._AddExpander()
self._GuiLogo()
def _GuiHeader(self):
"""Creates all UI elements above the input fields."""
self.root.columnconfigure(0, weight=1)
self.root.overrideredirect(1)
top = self.root.winfo_toplevel()
top.rowconfigure(0, weight=1)
top.columnconfigure(0, weight=1)
def _GuiLogo(self):
"""Creates the UI graphical logo."""
self.logo_frame = tk.Frame(self.root)
self.logo_frame.columnconfigure(0, weight=1)
r = resources.Resources()
path = r.GetResourceFileName('logo.gif')
self.logo_img = tk.PhotoImage(file=path)
self.logo = tk.Label(self.logo_frame, image=self.logo_img, text='logo here')
self.logo.grid(column=0, row=0, sticky='SE')
self.logo_frame.grid(column=0, row=self.row, sticky='EW')
self.row += 1
def _LoadOptions(self, options):
"""Load all options from the options file input.
UI elements are created for each option
Args:
options: a list of all options pending for the user
"""
for option in options:
if 'type' not in option:
logging.error('Untyped option: %s.', option)
continue
if option['type'] == 'radio_menu':
self.fields[option['name']] = fields.RadioMenu(self.root, option)
self.fields[option['name']].grid(column=0, row=self.row, pady=5)
elif option['type'] == 'toggle':
self.fields[option['name']] = fields.Toggle(self.root, option)
self.fields[option['name']].grid(column=0, row=self.row, pady=5)
else:
logging.error('Unknown option type: %s.', option['type'])
continue
self.root.rowconfigure(self.row, weight=0)
self.row += 1
self._AddSeparator()
def _Quit(self):
"""Save all responses and exit the UI."""
for field in self.fields:
self.responses[field] = self.fields[field].Value()
self.root.destroy()
def Responses(self):
return self.responses

161
chooser/chooser_test.py Normal file
View File

@@ -0,0 +1,161 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.chooser.chooser."""
from fakefs import fake_filesystem
from glazier.chooser import chooser
import mock
import unittest
_TEST_CONF = [{
'name':
'system_locale',
'type':
'radio_menu',
'prompt':
'System Locale',
'options': [
{
'label': 'de-de',
'value': 'de-de',
'tip': ''
},
{
'label': 'en-gb',
'value': 'en-gb',
'tip': ''
},
{
'label': 'en-us',
'value': 'en-us',
'tip': '',
'default': True
},
{
'label': 'es-es',
'value': 'es-es',
'tip': ''
},
{
'label': 'fr-fr',
'value': 'fr-fr',
'tip': ''
},
{
'label': 'ja-jp',
'value': 'ja-jp',
'tip': ''
},
{
'label': 'ko-kr',
'value': 'ko-kr',
'tip': ''
},
{
'label': 'zh-cn',
'value': 'zh-cn',
'tip': ''
},
{
'label': 'zh-hk',
'value': 'zh-hk',
'tip': ''
},
{
'label': 'zh-tw',
'value': 'zh-tw',
'tip': ''
},
]
}, {
'name':
'puppet_enable',
'type':
'toggle',
'prompt':
'Enable Puppet',
'options': [
{
'label': 'False',
'value': False,
'tip': '',
'default': True
},
{
'label': 'True',
'value': True,
'tip': ''
},
]
}]
class ChooserTest(unittest.TestCase):
@mock.patch.object(chooser, 'tk', autospec=True)
def setUp(self, unused_tk):
self.ui = chooser.Chooser(_TEST_CONF, preload=False)
v1 = mock.Mock()
v1.Value.return_value = 'value1'
v2 = mock.Mock()
v2.Value.return_value = 'value2'
v3 = mock.Mock()
v3.Value.return_value = 'value3'
self.ui.fields = {'field1': v1, 'field2': v2, 'field3': v3}
self.fs = fake_filesystem.FakeFilesystem()
chooser.resources.os = fake_filesystem.FakeOsModule(self.fs)
self.fs.CreateFile('/resources/logo.gif')
@mock.patch.object(chooser.fields, 'Timer', autospec=True)
def testDislpay(self, timer):
self.ui.timer = timer.return_value
self.ui.Display()
@mock.patch.object(chooser, 'tk', autospec=True)
@mock.patch.object(chooser.fields, 'Timer', autospec=True)
def testGuiFooter(self, unused_timer, unused_tk):
self.ui._GuiFooter()
def testGuiHeader(self):
self.ui._GuiHeader()
@mock.patch.object(chooser.fields, 'RadioMenu', autospec=True)
@mock.patch.object(chooser.fields, 'Separator', autospec=True)
@mock.patch.object(chooser.fields, 'Toggle', autospec=True)
def testLoadOptions(self, toggle, unused_sep, radio):
self.ui._LoadOptions(_TEST_CONF)
self.assertEqual(radio.call_args[0][1]['name'], 'system_locale')
self.assertEqual(toggle.call_args[0][1]['name'], 'puppet_enable')
# bad options
self.ui._LoadOptions([{
'name': 'notype'
}, {
'name': 'system_locale',
'type': 'radio_menu'
}, {
'name': 'unknown',
'type': 'unknown'
}])
def testQuit(self):
self.ui._Quit()
responses = self.ui.Responses()
self.assertEqual(responses['field2'], 'value2')
self.assertEqual(responses['field3'], 'value3')
if __name__ == '__main__':
unittest.main()

135
chooser/fields.py Normal file
View File

@@ -0,0 +1,135 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Form fields available for display in the chooser UI.
The chooser UI displays a dynamic series of fields to the user. In order to
cope with a variable quantity of fields, each set of options is enclosed
in a Frame, with Frames stacked in a single column in the top level.
Each Frame type that accepts user input is expected to offer a public Value
function which will return the state of the field in a yaml-compatible format.
This is called at the UI exit for final response storage.
"""
import Tkinter as tk
class RadioMenu(tk.Frame):
"""Radio menu provides a dropdown menu containing radio buttons.
One and only one menu element can be selected from the list.
"""
def __init__(self, root, option):
tk.Frame.__init__(self, root)
self.label = tk.Label(self, text=option['prompt'])
self.label.grid(row=0, column=0, padx=20)
self.button = tk.Menubutton(self, text='Choose One', relief=tk.GROOVE)
self.menu = tk.Menu(self.button)
self.button['menu'] = self.menu
self.select = tk.StringVar()
for opt in option['options']:
self.menu.add_radiobutton(label=opt['label'], variable=self.select,
value=opt['value'], command=self._Update)
if 'default' in opt:
self.select.set(opt['value'])
self._Update()
self.button.grid(row=0, column=1)
def _Update(self):
current = self.Value()
self.button.configure(text=current)
def Value(self):
return self.select.get()
class Separator(tk.Frame):
"""A decorative separator."""
def __init__(self, root):
tk.Frame.__init__(self, root, height=2, bd=1, relief=tk.SUNKEN)
class Label(tk.Frame):
"""A text label."""
def __init__(self, root, text, font_name='Helvetica', font_size=16):
tk.Frame.__init__(self, root)
self.label = tk.Label(self, text=text, font=font_name, font_size=font_size)
self.label.grid(row=0, column=0, padx=20)
class Timer(tk.Frame):
"""Countdown timer with Image Now button for UI footer."""
def __init__(self, root, timeout=60):
tk.Frame.__init__(self, root)
self.root = root
self._counter = timeout
self.countdown_1 = tk.Label(self, text='Build will start in...',
font=('Helvetica', 16))
self.countdown_2 = tk.Label(self, text=self._counter,
font=('Helvetica', 16))
self.countdown_3 = tk.Label(self, text='... or ...')
self.image_now = tk.Button(self, text='Image Now', command=self._Quit,
font=('Helvetica', 18))
self.countdown_1.grid(row=0, column=0)
self.countdown_2.grid(row=0, column=1)
self.countdown_3.grid(row=0, column=2)
self.image_now.grid(row=0, column=3)
def Pause(self, event):
self.countdown_1.configure(text='Automatic build paused...')
self.countdown_2.configure(text='')
self.countdown_3.configure(text='')
self._counter = -1
def Countdown(self):
if self._counter < 0: # user interrupt
return
if self._counter == 0: # timeout
self._Quit()
self._counter -= 1
self.countdown_2.configure(text=self._counter)
self.callback_id = self.root.after(1000, self.Countdown)
def _Quit(self):
self.root.after_cancel(self.callback_id)
self.root.quit()
class Toggle(tk.Frame):
"""An set of radio buttons with On (True)/Off (False) values."""
def __init__(self, root, option):
tk.Frame.__init__(self, root)
self.label = tk.Label(self, text=option['prompt'])
self.label.grid(row=0, column=0, padx=20)
self.state = tk.BooleanVar()
self.on_button = tk.Radiobutton(self, text='On', variable=self.state,
value=True)
self.off_button = tk.Radiobutton(self, text='Off', variable=self.state,
value=False)
for opt in option['options']:
if 'default' in opt:
self.state.set(opt['value'])
self.on_button.grid(row=0, column=1)
self.off_button.grid(row=0, column=2)
def Value(self):
return self.state.get()

102
chooser/fields_test.py Normal file
View File

@@ -0,0 +1,102 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.chooser.fields."""
from glazier.chooser import fields
import mock
import unittest
@mock.patch.object(fields, 'tk', autospec=True)
class FieldsTest(unittest.TestCase):
@mock.patch.object(fields, 'tk', autospec=True)
def setUp(self, tk):
self.root = tk.Tk()
def testLabel(self, unused_tk):
fields.Label(self.root, 'some label')
def testSeparator(self, unused_tk):
fields.Separator(self.root)
def testToggle(self, unused_tk):
opts = {'prompt': 'enable puppet',
'options': [{'label': 'true', 'value': True, 'default': True},
{'label': 'false', 'value': False}]}
toggle = fields.Toggle(self.root, opts)
toggle.state.set.assert_called_with(True)
toggle.Value()
class RadioMenuTest(unittest.TestCase):
@mock.patch.object(fields, 'tk', autospec=True)
def setUp(self, tk):
self.tk = tk
self.root = tk.Tk()
opts = {
'prompt': 'choose locale',
'options': [
{'label': 'en-gb', 'value': 'en-gb', 'tip': ''},
{'label': 'en-us', 'value': 'en-us', 'tip': '', 'default': True},
{'label': 'es-es', 'value': 'es-es', 'tip': ''}
]}
tk.StringVar.return_value.get.return_value = 'en-us'
self.rm = fields.RadioMenu(self.root, opts)
def testRadioMenu(self):
self.rm.select.set.assert_called_with('en-us')
self.rm.button.configure.assert_called_with(text='en-us')
class TimerTest(unittest.TestCase):
class Quit(Exception):
pass
@mock.patch.object(fields, 'tk', autospec=True)
def setUp(self, tk):
self.root = tk.Tk()
self.root.quit.side_effect = TimerTest.Quit
self.timer = fields.Timer(self.root, timeout=10)
def testPause(self):
self.timer.Pause(None)
self.assertEqual(self.timer._counter, -1)
def testCountdown(self):
# countdown
self.assertEqual(self.timer._counter, 10)
self.timer.Countdown()
self.assertEqual(self.timer._counter, 9)
self.assertTrue(self.root.after.called)
self.assertFalse(self.root.quit.called)
self.root.reset_mock()
# timeout
self.timer._counter = 0
self.assertRaises(TimerTest.Quit, self.timer.Countdown)
self.assertFalse(self.root.after.called)
self.assertTrue(self.root.quit.called)
self.root.reset_mock()
# interrupt
self.timer._counter = -1
self.timer.Countdown()
self.assertFalse(self.root.after.called)
self.assertFalse(self.root.quit.called)
if __name__ == '__main__':
unittest.main()

12
doc/index.md Normal file
View File

@@ -0,0 +1,12 @@
# Glazier Documentation
## Configuration
* [Glazier Build YAML Specification](yaml/index.md)
* [Tips for Writing Effective Glazier Configs](yaml/tips.md)
* [Actions README](../lib/actions/README.md)
* [Policies README](../lib/policies/README.md)
## Setup
* [Setup Guide](setup/index.md)

41
doc/setup/index.md Normal file
View File

@@ -0,0 +1,41 @@
# Setup Guide
[TOC]
## Distribution
Glazier requires a web based repository of binary and image files to be
available over HTTP(S). You can use any web server or platform that suits your
needs.
Inside the root of your web host, create two directories: the config root and
the binary root.
### Config Root
The configuration root must contain at minimum one `build.yaml` file. In a
mature system, this directory will likely contain a variety of branching config
files and scripts.
We recommend keeping the entire contents of the config root in source control,
and exporting it out to the web service whenever changes are made.
### Binary Root
The binary root is a separate directory structure used to hold non-text data.
This split serves to draw a clean boundary between files which may be sourced
from version control, and those which may instead live in mass storage
elsewhere.
We recommend using an organized tree structure to make binaries easy to locate.
* Root/
* Company1/
* Product1/
* v1/
* v2/
* ...
* Product2/
* v1/
* ...
* ...

BIN
doc/yaml/chooser_frames.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

98
doc/yaml/chooser_ui.md Normal file
View File

@@ -0,0 +1,98 @@
# Chooser UI
[TOC]
The Chooser setup UI is an enhancement to autobuild which allows Glazier to
present the user with a dynamic list of options as part of the installation
process.
## Architecture
When the configuration handler code reaches a 'choice' option, that element is
stored in buildinfo as pending. The system will continue to accumulate pending
options until the config calls an action to display the UI.
The UI display action retrieves all collected options and passes them to a fresh
UI instance. The Chooser UI is responsible for presenting the options and
collecting any responses from the user.
The Chooser dynamically populates the visible UI from top to bottom. Each field
is contained in a frame, allowing the overall structure to flow downwards, even
though different fields may contain more or fewer individual elements. If the
user does not engage the UI, the responses are still populated using the default
selections.
![Chooser Frames](chooser_frames.png)
Once the UI exits, the Chooser will make the responses available via a
dictionary to the caller. The resulting "response" values are returned to
buildinfo where they will be saved in state. These same values (dynamically
named as USER\_\*) can be referenced via pinning at any point later on in the
build.
## Syntax
The Glazer YAML specification allows Chooser options to be encoded as part of
the build config files. Autobuild compiles and translates these options into an
option file for the chooser in stage15. Leveraging the build YAMLs allows for
all the same pinning and templating capabilities as the other commands, meaning
Chooser options can be targeted at images on the fly based on any available
buildinfo data.
The top level YAML command *choice* indicates a chooser option. Each choice
consists of several required sub-fields:
### name
Name designates the option's internal name, and should be unique. Buildinfo will
aggregate all options as USER_\[name\] where name is determined by this field.
### type
Type indicates the UI field type to be shown (see below).
### prompt
Prompt is the text label shown in the UI next to the interactive fields.
### options
An ordered list of dictionaries containing all options to be presented. Each
dictionary in the list should have the following sub-fields.
* label: The label shown in the UI next to the selector.
* value: The value to be stored in the backend if this option is chosen.
* tip: Tooltip (currently not implemented)
* default: Set to boolean True to indicate the default selection. The field
can be skipped for all non-defaults.
## Field Types
### radio_menu
The radio_menu field provides a multiple choice drop-down menu. The menu allows
one and only one selection at a time from the available options.
choice:
    name: system_locale
    type: radio_menu
    prompt: 'System Locale'
    options: [
        {label: 'de-de', value: 'de-de', tip: ''},
        {label: 'en-gb', value: 'en-gb', tip: ''},
        {label: 'en-us', value: 'en-us', tip: '', default: True},
        ...
    ]
### toggle
A simple pair of on/off (or true/false) radio buttons.
choice:
    name: puppet_enable
    type: toggle
    prompt: 'Enable Puppet'
    options: [
        {label: 'False', value: False, tip: '', default: True},
        {label: 'True', value: True, tip: ''},
    ]

186
doc/yaml/index.md Normal file
View File

@@ -0,0 +1,186 @@
# Glazier Build YAML Specification
[TOC]
Glazier uses YAML-based configuration files. These documents outline the
supported syntax.
templates:
  software:
    include:
      - ['software/', 'build.yaml']
  some_template:
    manifest:
        - '#some_executable.exe'
    pull:
        ['somefile.txt', 'C:\somefile.txt']
controls:
  - pin:
      'os_code': ['win2008-x64-se', 'win2008-x64-ee']
    template:
      - software
  - pin:
      'os_code': ['win7']
    template:
      - some_template
## Top Level
The top level is a dictionary of two elements, *templates* and *controls*.
### templates
Templates is a dictionary of named elements. The name is used to reference each
template from one or more control elements. Templates are not executed unless
referenced by a control element. Their primary purpose is to allow a logical
grouping of commands which may be recycled more than once to simplify the
overall configuration.
The second template level is the template name. Names can be arbitrary, but
ideally should have some relevance to what the template is doing.
Beneath the template name is the common command element structure described
below. Pins are not used in templates, as it is assumed they will be called from
a pinned control instead.
### controls
Controls is an ordered list of unnamed elements. The list structure is used to
provide a consistent ordering of elements from top to bottom, so commands can be
executed in a predictable order. All build yamls execute commands from top to
bottom.
The second control level is the common command structure detailed below. A
control commonly starts with a pin item, unlike templates, but a pin is not
required. Unpinned controls will match all.
## Command Elements
Each individual block of the controls list or the templates dictionary can
contain any combination of the following, except for pins, which are exclusive
to control elements.
The order in which individual elements within a single command group are
processed is determined by the build code and may be subject to change. If you
need to control the order of operations, split the commands between multiple
command groups, as the groups are always process sequentially.
### Actions
Actions are dynamic command elements. Unlike the static commands listed on this
page, actions are not hardcoded into the config handler. When a configuration
file references a command that is not one of the known static commands, the
config handler will attempt to look up the class name in the actions module. If
it finds it, the class is loaded and run with the arguments from the
configuration file entry.
Actions are the preferred method for adding new functionality to the autobuild
tool. Unlike hardcoded commands, actions are almost fully self contained and
capable of self-validating.
See [the Actions README](../../lib/actions/README.md) for a list of available
actions.
### Pin
Exclusive to control elements, the pin attaches the current block to a specific
set of build info tags. The tags are inclusive, and must *all *match in order
for the command block to be executed by the build. The format is a dictionary,
where the key is the variable name from buildinfo and the value is a list of
acceptable values. If the key value in buildinfo matches any of the strings in
the list, the pin passes for that key.
Some pins support "loose" matching. In the case of loose matches, the entire pin
string is checked against the start of every corresponding buildinfo value. For
example: 'A-B' matches 'A-B' as well as 'A-B-C-D', but not 'A-C'.
- pin:
'os_code': ['win7']
'department': ['demo']
Inverse pinning is also supported. Inverse pins are like regular pins, with the
match string beginning with an exclamation point (!). An inverse pin returns
False if any one buildinfo value matches the inverse string (minus the !). For
example: `'os_code': ['!win7']` excludes the pin from os_code=win7 hosts.
While direct match pins are exclusive, skipping any values not named in the set,
inverse match pins are inclusive, accepting any values not named directly. If
the pin is not negated by a matching inverse pin, the outcome is a successful
match. For example: `'os_code': ['!win7', '!win8']` is False for os_code=win7
and False for os_code=win8, but True for os_code=win2012.
*Direct pins are only considered if no inverse pins are present.* This is to
compensate for direct matches being exclusive in nature. It would not make sense
to supply \[!A, !B, C\], because \[C\] would have the same result.
Pins are generally treated as case insensitive.
### Policy
The policy tag specifies an imaging policy. Imaging policies are used to verify
that the state of the host being installed meets a given set of expectations.
Each policy tag element is a single string consisting of the name of the imaging
policy class to be enforced. The class name must match exactly, as classes are
dynamically referenced.
- policy:
- 'DeviceModel'
See also [the Policies README](../../lib/policies/README.md)
### Template
The template tag tells build to process a list of one or more named templates.
Templates are processed recursively, so templates can call other templates as
well.
template:
      - workstation
### Include
The include tag tells build to process an additional yaml file. The structure is
a list of two part entries, a directory name relative to the current build
directory, and a build file name. Includes are useful for breaking up large
build files into smaller logical groups.
include:
      - ['demo/', 'build.yaml']
## Supported Pins
The pins are essentially exported build info variables that help identify the
installing host. Not all of build info is exported for the purposes of pinning,
although it's always possible to extend the code to support different pins in
the future.
* computer_model
* The computer model hardware string (eg HP Z620 Workstation)
* Supports partial model matching from the left.
* device_id
* A hardware device id string in the format of
\[vendor-device-subsystem-revision\]. Will be matched against every
device id detected in hardware.
* Supports partial device matching from the left (eg AA-BB in the config
will match AA-BB-CC-DD in hardware).
* encryption_type
* TPM, Startup Key, etc
* graphics
* Detected graphics cards (by name).
* os_code
* Corresponds to the generic operating system code as defined in
release-info.yaml. Used for generalized identification of the target
platform.
## Misc
### Comments
The yaml specification allows comments by prefacing lines with a hash (#). Feel
free to comment the configs to improve readability.
## Configuration Handlers
See the [Glazier Configuration Handlers](config_handlers.md) page for more
information about how the configuration files are processed.

26
doc/yaml/tips.md Normal file
View File

@@ -0,0 +1,26 @@
# Tips for Writing Effective Glazier Configs
* YAML supports comments. Use them to delinate/decorate config blocks as well
as communicating intent or documenting bugs/TODOs.
* Some parts of configs are strictly ordered and others are not. The top
level, implemented as an ordered list, will always happen in sequence. Be
careful not to assume strict ordering in other parts of the config,
particularly where the YAML is dictionary typed. When in doubt, use two top
level config elements to assert order of operations.
* You can also achieve ordering with list-based types, such as templates
and includes.
* Use includes to batch together a series of commands all affected by the same
Pins. Rather than applying the same Pins to each of a series of configs, put
the entire series in a separate file. Then apply the shared Pin(s) to the
include statement that references the new config file.
* Use includes and directory structure to break up the configuration flow in a
logical way. Everything could live in one file and directory if you wanted
it to, but it would be ugly and hard to read.
* It's easier to cherrypick changes from separate files. Consider separating
out elements that are frequently changed for easier management across
branches.

13
lib/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

409
lib/actions/README.md Normal file
View File

@@ -0,0 +1,409 @@
# Glazier Installer Actions
[TOC]
Actions are classes which the configuration handler may call to perform a
variety of tasks during imaging.
## Usage
Each module should inherit from BaseAction and will receive a BuildInfo instance
(self.\_build_info).
Arguments are stored as a data structure in the self.\_args variable, commonly
as an ordered list or dictionary.
Each action class should override the Run function to execute its main behavior.
If an action fails, the module should raise ActionError with a message
explaining the cause of failure. This will abort the build.
## Validation
Config validation can be accomplished by overriding the Validate() function.
Validate should test the inputs (\_args) for correctness. If \_args contains any
unexpected or inappropriate data, ValidationError should be raised.
Validate() will pass for any actions which have not overridden it with custom
rules.
## Actions
### Abort
Aborts the build with a custom error message.
#### Arguments
* Format: List
* Arg1[str]: An error message to display; the reason for aborting.
### AddChoice
Aliases: choice
### BitlockerEnable
Enable Bitlocker on the host system.
Available modes:
* ps_tpm: TPM via Powershell
* bde_tpm: TPM via manage-bde.exe
#### Arguments
* Format: List
* Arg1[str]: The mode to use for enabling Bitlocker.
### BuildInfoDump
Write state from the BuildInfo class to disk for later processing by
BuildInfoSave.
### BuildInfoSave
Load BuildInfo data from disk and store permanently to the registry.
### CopyFile/MultiCopyFile
Copy files from source to destination.
Also available as MultiCopyFile for copying larger sets of files.
#### CopyFile Arguments
* Format: List
* Arg1[str]: Source file path
* Arg2[str]: Destination file path.
#### MultiCopyFile Arguments
* Format: List
* Arg1[list]: First set of files to copy
* Arg1[str]: Source file path
* Arg2[str]: Destination file path.
* Arg2[list]: Second set of files to copy
* Arg1[str]: Source file path
* Arg2[str]: Destination file path.
* ...
#### Examples
CopyFile: ['X:\glazier.log', 'C:\Windows\Logs\glazier.log']
MultiCopyFile:
- ['X:\glazier-applyimg.log', 'C:\Windows\Logs\glazier-applyimg.log']
- ['X:\glazier.log', 'C:\Windows\Logs\glazier.log']
### DomainJoin
Joins the host to the domain. (Requires installer to be running within the host
OS.)
#### Arguments
* Format: List
* Arg1[str]: The desired method to use for the join, as defined by the
domain join library.
* Arg2[str]: The name of the domain to join.
* Arg3[str]: The OU to join the machine to. (optional)
#### Example
DomainJoin: ['interactive', 'domain.example.com']
DomainJoin: ['auto', 'domain.example.com', 'OU=Servers,DC=DOMAIN,DC=EXAMPLE,DC=COM']
### Driver
Process drivers in WIM format. Downloads file, verifies hash, creates an empty
directory, mounts wim file, applies drivers to the base image, and finally
unmounts wim.
#### Example
Driver: ['@/Driver/HP/z840/win10/20160909/z840.wim',
'C:\Glazier_Cache\z840.wim',
'cd8f4222a9ba4c4493d8df208fe38cdad969514512d6f5dfd0f7cc7e1ea2c782']
### Execute
Run one or more commands on the system.
Supports multiple commands via nested list structure due to the frequency of
program executions occurring as part of a typical imaging process.
#### Arguments
* Format: List
* Arg1[list]: The first command to execute
* ArgA[str]: The entire command line to execute including flags.
* ArgB[list]: One or more integers indicating successful exit codes.
* Default: [0]
* ArgC[list]: One or more integers indicating that a reboot is
required.
* Default: []
* ArgD[bool]: Rerun after a reboot occurs. A reboot code must be
provided and returned by the execution.)
* Default: False
* Arg2[list]: The second command to execute. (optional)
* ...
#### Examples
Execute: [
# Using defaults.
['C:\Windows\System32\netsh.exe interface teredo set state disabled'],
# 0 or 1 are successful exit codes, 3010 will trigger a restart.
['C:\Windows\System32\msiexec.exe /i @Drivers/HP/zbook/HP_Hotkey_Support_6_2_20_8.msi /qn /norestart', [0,1], [3010]],
# 0 is a successful exit code, 2 will trigger a restart, and 'True' will rerun the command after the restart.
['C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -File #secureboot.ps1', [0], [2], True]
]
### ExitWinPE
Leave the WinPE environment en route to the local host configuration. Is
normally followed by sysprep, then the relaunch of the autobuild tool running
inside the new host image.
Performs multiple steps in one:
* Copies the autobuild executable to C:
* Copies the acting task list to C:
* Reboots the host
Without a separate command, some of these actions would remain in the task list
after being carried over, and would be re-executed.
#### Arguments
* None
### Get
Aliases: pull
Downloads remote files to local disk. Get is an ordered, two dimensional list of
source file names and destination file names. Source filenames are assumed to be
relative to the location of the current yaml file.
#### Verification
To use checksum verification, add the computed SHA256 hash as a third argument
to the list. This argument is optional, and being absent or null bypasses
verification.
Get:
    - ['windows10.wim', 'c:\base.wim', '4b5b6bf0e59dadb4663ad9b4110bf0794ba24c344291f30d47467d177feb4776']
#### Arguments
* Format: List
* Arg1[list]: The first file to retrieve.
* ArgA[str]: The remote path to the source file.
* ArgB[str]: The local destination path for the file.
* ArgC[str]: The sha256 sum of the flie for verification. (optional)
* Arg2[list]: The second file to retrieve. (optional)
* ...
#### Examples
Get:
- ['win2008-x64-se.wim', 'c:\base.wim']
- ['win2008-x64-se.wim.sha256', 'c:\base.wim.sha256']
### LogCopy
Attempts to copy a log file to a new destination for collection.
Destinations include Event Log and CIFS. Copy failures only produce warnings
rather than hard failures.
Logs will always be copied to the local Application log. Specifying the second
logs share parameter will also attempt to copy the log to the specified file
share.
#### Arguments
* Format: List
* Arg1[str]: Full path name of the source log file.
* Arg2[str]: The path to the destination file share. (optional)
#### Examples
LogCopy: ['C:\Windows\Logs\glazier.log', '\\shares.example.com\logs-share']
### MkDir
Make a directory.
#### Arguments
* Format: List
* Arg1[str]: Full path name of directory
#### Examples
MkDir: ['C:\Glazier_Cache']
### PSScript
Run a Powershell script file using the local Powershell interpreter.
#### Arguments
* Format: List
* Arg1[str]: The script file name or path to be run.
* Arg2[list]: A list of flags to be supplied to the script. (Optional)
#### Examples
PSScript: ['#Sample-Script.ps1']
PSScript: ['C:\Sample-Script2.ps1', ['-Flag1', 123, '-Flag2']]
### RegAdd
Create a registry key.
#### Arguments
* Format: List
* Arg1[str]: Root key
* Arg2[str]: Key path
* Arg3[str]: Key name
* Arg4[str]: Key value
* Arg5[str]: Key type
* Arg6[bool]: Use 64bit Registry (Optional)
#### Examples
RegAdd: ['HKLM', 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform', 'KeyManagementServiceName', 'kms.example.com', 'REG_SZ']
### Reboot
Restart the host machine.
#### Arguments
* Format: List
* Arg1[int]: The timeout delay until restart occurs.
* Arg2[str]: The reason/message for the restart to be displayed.
(Optional)
#### Examples
Reboot: [30]
Reboot: [10, "Restarting to finish installing drivers."]
### SetUnattendTimeZone
Attempts to detect the timezone via DHCP and configures any \<TimeZone\> fields
in unattend.xml with the resulting values.
#### Arguments
* None
### SetupCache
Creates the imaging cache directory with the path stored in BuildInfo.
#### Arguments
* None
#### Examples
SetupCache: []
### SetTimer
Add an imaging timer.
#### Arguments
* Format: List
* Arg1[str]: Timer name
#### Examples
SetTimer: ['TimerName']
### ShowChooser
Show the Chooser UI to display all accumulated options to the user. All results
are returned to BuildInfo and the pending options list is cleared.
#### Arguments
* None
### Shutdown
Shutdown the host machine.
#### Arguments
* Format: List
* Arg1[int]: The timeout delay until shutdown occurs.
* Arg2[str]: The reason/message for the shutdown to be displayed.
(Optional)
#### Examples
Shutdown: [30]
Shutdown: [10, "Shutting down to save power."]
### Sleep
Pause the installer.
#### Arguments
* Format: List
* Arg1[int]: Duration to sleep.
#### Examples
Sleep: [30]
### Unzip
Unzip a zip file to the local filesystem.
#### Arguments
* Format: List
* Arg1[str]: Path to the zip file.
* Arg2[str]: Path to extract the zip file to.
#### Examples
Unzip: ['C:\some_archive.zip', 'C:\Some\Destination\Path']
### UpdateMSU
Process updates in MSU format. Downloads file, verifies hash, creates a
SYS_CACHE\Updates folder that is used as a temp location to extract the msu
file, and applies the update to the base image.
#### Example
Update: ['@/Driver/HP/z840/win7/20160909/kb290292.msu',
'C:\Glazier_Cache\kb290292.msu',
'cd8f4222a9ba4c4493d8df208fe38cdad969514512d6f5dfd0f7cc7e1ea2c782']
### Warn
Issue a warning that can be bypassed by the user.
#### Arguments
* Format: List
* Arg1[string]: Message to the user.
#### Examples
Warn: ["You probably don't want to do this, or bad things will happen."]

72
lib/actions/__init__.py Normal file
View File

@@ -0,0 +1,72 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Simplify access to Glazier action modules."""
from glazier.lib.actions import abort
from glazier.lib.actions import base
from glazier.lib.actions import domain
from glazier.lib.actions import drivers
from glazier.lib.actions import file_system
from glazier.lib.actions import files
from glazier.lib.actions import installer
from glazier.lib.actions import powershell
from glazier.lib.actions import registry
from glazier.lib.actions import sysprep
from glazier.lib.actions import system
from glazier.lib.actions import timers
from glazier.lib.actions import tpm
from glazier.lib.actions import updates
# pylint: disable=invalid-name
Abort = abort.Abort
AddChoice = installer.AddChoice
BitlockerEnable = tpm.BitlockerEnable
BuildInfoDump = installer.BuildInfoDump
BuildInfoSave = installer.BuildInfoSave
CopyFile = file_system.CopyFile
DomainJoin = domain.DomainJoin
DriverWIM = drivers.DriverWIM
Execute = files.Execute
ExitWinPE = installer.ExitWinPE
Get = files.Get
LogCopy = installer.LogCopy
MkDir = file_system.MkDir
MultiCopyFile = file_system.MultiCopyFile
PSScript = powershell.PSScript
Reboot = system.Reboot
RegAdd = registry.RegAdd
SetTimer = timers.SetTimer
SetUnattendTimeZone = sysprep.SetUnattendTimeZone
SetupCache = file_system.SetupCache
ShowChooser = installer.ShowChooser
Shutdown = system.Shutdown
Sleep = installer.Sleep
Unzip = files.Unzip
UpdateMSU = updates.UpdateMSU
Warn = abort.Warn
ActionError = base.ActionError
ValidationError = base.ValidationError
RestartEvent = base.RestartEvent
ShutdownEvent = base.ShutdownEvent
# Legacy naming
choice = installer.AddChoice
copy = file_system.MultiCopyFile
driver = drivers.DriverWIM
pull = files.Get
run = files.Execute

60
lib/actions/abort.py Normal file
View File

@@ -0,0 +1,60 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for stopping the image."""
import logging
import re
from glazier.lib import interact
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import ValidationError
class Abort(BaseAction):
"""Abort imaging with a custom message."""
def Run(self):
message = self._args[0]
raise ActionError(str(message))
def Validate(self):
if not isinstance(self._args, list):
raise ValidationError('Invalid args type (%s): %s' %
(type(self._args), self._args))
if len(self._args) is not 1:
raise ValidationError('Invalid args length: %s' % self._args)
if not isinstance(self._args[0], str):
raise ValidationError('Invalid argument type: %s' % self._args[0])
class Warn(BaseAction):
"""Warn the user about a problem condition, and ask whether to continue."""
def Run(self):
print '\n\n%s\n\n' % str(self._args[0])
response = interact.Prompt('Do you still want to proceed (y/n)? ')
if not response or not re.match(r'^[Yy](es)?$', response):
raise ActionError('User chose not to continue installation.')
logging.info('User chose to continue installation despite warning.')
def Validate(self):
if not isinstance(self._args, list):
raise ValidationError('Invalid args type (%s): %s' %
(type(self._args), self._args))
if len(self._args) is not 1:
raise ValidationError('Invalid args length: %s' % self._args)
if not isinstance(self._args[0], str):
raise ValidationError('Invalid argument type: %s' % self._args[0])

62
lib/actions/abort_test.py Normal file
View File

@@ -0,0 +1,62 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.abort."""
from glazier.lib.actions import abort
import mock
import unittest
class AbortTest(unittest.TestCase):
@mock.patch('glazier.lib.buildinfo.BuildInfo', autospec=True)
def testAbort(self, build_info):
ab = abort.Abort(['abort message'], build_info)
self.assertRaises(abort.ActionError, ab.Run)
def testAbortValidate(self):
ab = abort.Abort('abort message', None)
self.assertRaises(abort.ValidationError, ab.Validate)
ab = abort.Abort([1, 2, 3], None)
self.assertRaises(abort.ValidationError, ab.Validate)
ab = abort.Abort([1], None)
self.assertRaises(abort.ValidationError, ab.Validate)
ab = abort.Abort(['Error Message'], None)
ab.Validate()
@mock.patch('glazier.lib.buildinfo.BuildInfo', autospec=True)
@mock.patch('glazier.lib.interact.Prompt', autospec=True)
def testWarn(self, prompt, build_info):
warn = abort.Warn(['warning message'], build_info)
prompt.return_value = None
self.assertRaises(abort.ActionError, warn.Run)
prompt.return_value = 'no thanks'
self.assertRaises(abort.ActionError, warn.Run)
prompt.return_value = 'Y'
warn.Run()
def testWarnValidate(self):
warn = abort.Warn('abort message', None)
self.assertRaises(abort.ValidationError, warn.Validate)
warn = abort.Warn([1, 2, 3], None)
self.assertRaises(abort.ValidationError, warn.Validate)
warn = abort.Warn([1], None)
self.assertRaises(abort.ValidationError, warn.Validate)
warn = abort.Warn(['Error Message'], None)
warn.Validate()
if __name__ == '__main__':
unittest.main()

95
lib/actions/base.py Normal file
View File

@@ -0,0 +1,95 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generic imaging action class."""
import logging
#
# Error Types
#
class ActionError(Exception):
"""Failure completing requested action."""
class ValidationError(Exception):
"""Failure validating a command type."""
#
# Event Types
#
class PowerEvent(Exception):
def __init__(self,
message,
timeout,
retry_on_restart=False,
task_list_path=None):
super(PowerEvent, self).__init__(message)
self.retry_on_restart = retry_on_restart
self.task_list_path = task_list_path
self.timeout = timeout
class RestartEvent(PowerEvent):
"""Action requesting a host restart."""
class ShutdownEvent(PowerEvent):
"""Action reuqesting a host shutdown."""
class BaseAction(object):
"""Generic action type."""
def __init__(self, args, build_info):
self._args = args
self._build_info = build_info
self._realtime = False
self._Setup()
def IsRealtime(self):
"""Run the action on discovery rather than queueing in the task list."""
return self._realtime
def Run(self):
"""Override this function to implement a new action."""
pass
def _Setup(self):
"""Override to customize action on initialization."""
pass
def Validate(self):
"""Override this function to implement validation of actions."""
logging.warn('Validation not implemented for action %s.',
self.__class__.__name__)
def _ListOfStringsValidator(self, args, length=1, max_length=None):
if not max_length:
max_length = length
self._TypeValidator(args, list)
if not length <= len(args) <= max_length:
raise ValidationError('Invalid args length: %s' % args)
for arg in args:
self._TypeValidator(arg, str)
def _TypeValidator(self, args, expect_types):
if not isinstance(args, expect_types):
raise ValidationError('Invalid type for arg %s. Found: %s, Expected: %s' %
(args, type(args), str(expect_types)))

30
lib/actions/base_test.py Normal file
View File

@@ -0,0 +1,30 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.base."""
from glazier.lib.actions import base
import unittest
class BaseTest(unittest.TestCase):
def testRun(self):
b = base.BaseAction(None, None)
b.Run()
b.Validate()
if __name__ == '__main__':
unittest.main()

41
lib/actions/domain.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for interacting with the company domain."""
from glazier.lib import domain_join
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import ValidationError
class DomainJoin(BaseAction):
"""Create an imaging timer."""
def Run(self):
method = str(self._args[0])
domain = str(self._args[1])
ou = None
if len(self._args) > 2:
ou = str(self._args[2])
joiner = domain_join.DomainJoin(method, domain, ou)
try:
joiner.JoinDomain()
except domain_join.DomainJoinError as e:
raise ActionError('Unable to complete domain join. %s' % str(e))
def Validate(self):
self._ListOfStringsValidator(self._args, length=2, max_length=3)
if self._args[0] not in domain_join.AUTH_OPTS:
raise ValidationError('Invalid join method: %s' % self._args[0])

View File

@@ -0,0 +1,57 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.domain."""
from glazier.lib.actions import domain
import mock
import unittest
class DomainTest(unittest.TestCase):
@mock.patch.object(domain.domain_join, 'DomainJoin', autospec=True)
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testDomainJoin(self, build_info, join):
args = ['interactive', 'domain.test.com']
dj = domain.DomainJoin(args, build_info)
dj.Run()
join.assert_called_with('interactive', 'domain.test.com', None)
# default ou
args += ['OU=Test,DC=DOMAIN,DC=TEST,DC=COM']
dj = domain.DomainJoin(args, build_info)
dj.Run()
join.assert_called_with('interactive', 'domain.test.com',
'OU=Test,DC=DOMAIN,DC=TEST,DC=COM')
# error
join.return_value.JoinDomain.side_effect = (
domain.domain_join.DomainJoinError)
self.assertRaises(domain.ActionError, dj.Run)
def testDomainJoinValidate(self):
dj = domain.DomainJoin('interactive', None)
self.assertRaises(domain.ValidationError, dj.Validate)
dj = domain.DomainJoin([1, 2, 3], None)
self.assertRaises(domain.ValidationError, dj.Validate)
dj = domain.DomainJoin([1], None)
self.assertRaises(domain.ValidationError, dj.Validate)
dj = domain.DomainJoin(['unknown'], None)
self.assertRaises(domain.ValidationError, dj.Validate)
dj = domain.DomainJoin(['interactive', 'domain.test.com'], None)
dj.Validate()
if __name__ == '__main__':
unittest.main()

134
lib/actions/drivers.py Normal file
View File

@@ -0,0 +1,134 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for managing device drivers."""
import logging
import os
from glazier.lib import constants
from glazier.lib import file_util
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import ValidationError
from glazier.lib.actions.files import Execute
from glazier.lib.actions.files import Get
class DriverWIM(BaseAction):
"""Downloads file and verifies extension.
File is downloaded and processed based on supported file extension.
file can then be processed to be used by dism commands.
Method can be expanded to access and process other formats.
Also can be used to process multiple files.
Raises:
ActionError: Call with unsupported file type.
"""
FILE_EXT_SUPPORTED = ['.wim']
def Run(self):
for wim in self._args:
dst = str(wim[1])
file_ext = os.path.splitext(dst)[1]
if file_ext not in self.FILE_EXT_SUPPORTED:
raise ActionError('Unsupported driver file format %s.' % dst)
g = Get([wim], self._build_info)
g.Run()
logging.info('Found WIM file, processing drivers using DISM.')
self._ProcessWim(dst)
def Validate(self):
self._TypeValidator(self._args, list)
for cmd_arg in self._args:
self._TypeValidator(cmd_arg, list)
if not 2 <= len(cmd_arg) <= 3:
raise ValidationError('Invalid args length: %s' % cmd_arg)
self._TypeValidator(cmd_arg[0], str) # remote
self._TypeValidator(cmd_arg[1], str) # local
file_ext = os.path.splitext(cmd_arg[1])[1]
if file_ext not in self.FILE_EXT_SUPPORTED:
raise ValidationError('Invalid file type: %s' % cmd_arg[1])
if len(cmd_arg) > 2: # hash
for arg in cmd_arg[2]:
self._TypeValidator(arg, str)
def _AddDriver(self, mount_dir):
"""Command used to process drivers in a given directory.
This command will process all fo the .inf file in a folder recursively. It
can be used regardless of how the drivers are added to the local machine.
If the exit code for the parsed command is anything other than zero, report
fatal error.
Args:
mount_dir: local directory where the driver .inf files can be found.
Raises:
ConfigRunnerError: Error during driver application.
"""
dism = ['{} /Image:c: /Add-Driver /Driver:{} /Recurse'.format(
constants.WINPE_DISM, mount_dir)]
ex = Execute([dism], self._build_info)
try:
ex.Run()
except ActionError as e:
raise ActionError('Error applying drivers to image from %s. (%s)' %
(mount_dir, e))
def _ProcessWim(self, wim_file):
"""Processes WIM driver files using DISM commands.
Runs necessary commands to process a driver file in WIM format
Args:
wim_file: current file location.
Raises:
ConfigRunnerError: Failure mounting or unmounting WIM.
"""
mount_dir = '%s\\Drivers\\' % constants.SYS_CACHE
# dism commands
mount = [
'{} /Mount-Image /ImageFile:{} /MountDir:{} /ReadOnly /Index:1'.format(
constants.WINPE_DISM, wim_file, mount_dir)
]
unmount = ['{} /Unmount-Image /MountDir:{} /Discard'.format(
constants.WINPE_DISM, mount_dir)]
# create mount directory
file_util.CreateDirectories(mount_dir)
# mount image
ex = Execute([mount], self._build_info)
try:
ex.Run()
except ActionError as e:
raise ActionError('Unable to mount image %s. (%s)' % (wim_file, e))
logging.info('Applying %s image to main disk.', wim_file)
self._AddDriver(mount_dir)
# Unmount after running
ex = Execute([unmount], self._build_info)
try:
ex.Run()
except ActionError as e:
raise ActionError('Error unmounting image. Unable to continue. (%s)' % e)

101
lib/actions/drivers_test.py Normal file
View File

@@ -0,0 +1,101 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.drivers."""
from glazier.lib.actions import drivers
from glazier.lib.buildinfo import BuildInfo
import mock
import unittest
class DriversTest(unittest.TestCase):
@mock.patch.object(BuildInfo, 'ReleasePath')
@mock.patch('glazier.lib.download.Download.VerifyShaHash', autospec=True)
@mock.patch('glazier.lib.download.Download.DownloadFile', autospec=True)
@mock.patch.object(drivers, 'Execute', autospec=True)
@mock.patch.object(drivers.file_util, 'CreateDirectories', autospec=True)
def testDriverWIM(self, mkdir, exe, dl, sha, rpath):
bi = BuildInfo()
# Setup
remote = '@Drivers/Lenovo/W54x-Win10-Storage.wim'
local = r'c:\W54x-Win10-Storage.wim'
sha_256 = (
'D30F9DB0698C87901DF6824D11203BDC2D6DAAF0CE14ABD7C0A7B75974936748')
conf = {
'data': {
'driver': [[remote, local, sha_256]]
},
'path': ['/autobuild']
}
rpath.return_value = '/'
# Success
dw = drivers.DriverWIM(conf['data']['driver'], bi)
dw.Run()
dl.assert_called_with(
mock.ANY, ('https://glazier-server.example.com/'
'bin/Drivers/Lenovo/W54x-Win10-Storage.wim'),
local,
show_progress=True)
sha.assert_called_with(mock.ANY, local, sha_256)
cache = drivers.constants.SYS_CACHE
exe.assert_called_with([[('X:\\Windows\\System32\\dism.exe /Unmount-Image '
'/MountDir:%s\\Drivers\\ /Discard' % cache)]],
mock.ANY)
mkdir.assert_called_with('%s\\Drivers\\' % cache)
# Invalid format
conf['data']['driver'][0][1] = 'C:\\W54x-Win10-Storage.zip'
dw = drivers.DriverWIM(conf['data']['driver'], bi)
self.assertRaises(drivers.ActionError, dw.Run)
conf['data']['driver'][0][1] = 'C:\\W54x-Win10-Storage.wim'
# Mount Fail
exe.return_value.Run.side_effect = drivers.ActionError()
self.assertRaises(drivers.ActionError, dw.Run)
# Dism Fail
exe.return_value.Run.side_effect = iter([0, drivers.ActionError()])
self.assertRaises(drivers.ActionError, dw.Run)
# Unmount Fail
exe.return_value.Run.side_effect = iter([0, 0, drivers.ActionError()])
self.assertRaises(drivers.ActionError, dw.Run)
def testDriverWIMValidate(self):
g = drivers.DriverWIM('String', None)
self.assertRaises(drivers.ValidationError, g.Validate)
g = drivers.DriverWIM([[1, 2, 3]], None)
self.assertRaises(drivers.ValidationError, g.Validate)
g = drivers.DriverWIM([[1, '/tmp/out/path']], None)
self.assertRaises(drivers.ValidationError, g.Validate)
g = drivers.DriverWIM([['/tmp/src.zip', 2]], None)
self.assertRaises(drivers.ValidationError, g.Validate)
g = drivers.DriverWIM([['https://glazier/bin/src.wim', '/tmp/out/src.zip']],
None)
self.assertRaises(drivers.ValidationError, g.Validate)
g = drivers.DriverWIM([['https://glazier/bin/src.wim', '/tmp/out/src.wim']],
None)
g.Validate()
g = drivers.DriverWIM(
[['https://glazier/bin/src.wim', '/tmp/out/src.wim', '12345']], None)
g.Validate()
g = drivers.DriverWIM(
[['https://glazier/bin/src.zip', '/tmp/out/src.zip', '12345', '67890']],
None)
self.assertRaises(drivers.ValidationError, g.Validate)
if __name__ == '__main__':
unittest.main()

109
lib/actions/file_system.py Normal file
View File

@@ -0,0 +1,109 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for managing the local file systems."""
import logging
import os
import shutil
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import ValidationError # pylint:disable=unused-import
class FileSystem(BaseAction):
"""Parent filesystem class with utility functions."""
def _CreateDirectories(self, path):
"""Create a directory.
Args:
path: The full path to the directory to be created.
Raises:
ActionError: Failure creating the requested directory.
"""
if not os.path.isdir(path):
try:
logging.debug('Creating directory: %s', path)
os.makedirs(path)
except (shutil.Error, OSError) as e:
raise ActionError('Unable to create directory %s: %s' % (path, str(e)))
class CopyFile(FileSystem):
"""Copies files on disk."""
def Run(self):
try:
src = self._args[0]
dst = self._args[1]
except IndexError:
raise ActionError('Unable to determine source and destination from %s.' %
str(self._args))
try:
path = os.path.dirname(dst)
self._CreateDirectories(path)
shutil.copy2(src, dst)
logging.info('Copying: %s to %s', src, dst)
except (shutil.Error, IOError) as e:
raise ActionError('Unable to copy %s to %s: %s' % (src, dst, str(e)))
def Validate(self):
self._ListOfStringsValidator(self._args, length=2)
class MultiCopyFile(BaseAction):
"""Perform CopyFile on multiple sets of files."""
def Run(self):
try:
for arg in self._args:
cf = CopyFile([arg[0], arg[1]], self._build_info)
cf.Run()
except IndexError:
raise ActionError('Unable to determine copy sets from %s.' %
str(self._args))
def Validate(self):
self._TypeValidator(self._args, list)
for arg in self._args:
cf = CopyFile(arg, self._build_info)
cf.Validate()
class MkDir(FileSystem):
"""Create a directory."""
def Run(self):
try:
path = self._args[0]
except IndexError:
raise ActionError('Unable to determine desired path from %s.' %
str(self._args))
self._CreateDirectories(path)
def Validate(self):
self._ListOfStringsValidator(self._args)
class SetupCache(FileSystem):
"""Create the imaging cache directory."""
def Run(self):
path = self._build_info.Cache().Path()
self._CreateDirectories(path)
def Validate(self):
self._TypeValidator(self._args, list)

View File

@@ -0,0 +1,114 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.file_system."""
from fakefs import fake_filesystem
from fakefs import fake_filesystem_shutil
from glazier.lib.actions import file_system
import mock
import unittest
class FileSystemTest(unittest.TestCase):
def setUp(self):
# fake filesystem
fakefs = fake_filesystem.FakeFilesystem()
fakefs.CreateDirectory(r'/stage')
fakefs.CreateFile(r'/file1.txt', contents='file1')
fakefs.CreateFile(r'/file2.txt', contents='file2')
self.fake_open = fake_filesystem.FakeFileOpen(fakefs)
file_system.os = fake_filesystem.FakeOsModule(fakefs)
file_system.shutil = fake_filesystem_shutil.FakeShutilModule(fakefs)
file_system.open = self.fake_open
self.fakefs = fakefs
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testCopyFile(self, build_info):
src1 = r'/file1.txt'
dst1 = r'/windows/glazier/glazier.log'
src2 = r'/file2.txt'
dst2 = r'/windows/glazier/other.log'
cf = file_system.MultiCopyFile([[src1, dst1], [src2, dst2]], build_info)
cf.Run()
self.assertTrue(self.fakefs.Exists(r'/windows/glazier/glazier.log'))
self.assertTrue(self.fakefs.Exists(r'/windows/glazier/other.log'))
# bad path
src1 = r'/missing.txt'
cf = file_system.CopyFile([src1, dst1], build_info)
self.assertRaises(file_system.ActionError, cf.Run)
# bad args
cf = file_system.CopyFile([src1], build_info)
self.assertRaises(file_system.ActionError, cf.Run)
# bad multi args
cf = file_system.MultiCopyFile(src1, build_info)
self.assertRaises(file_system.ActionError, cf.Run)
def testCopyFileValidate(self):
cf = file_system.MultiCopyFile('String', None)
self.assertRaises(file_system.ValidationError, cf.Validate)
cf = file_system.MultiCopyFile(['String'], None)
self.assertRaises(file_system.ValidationError, cf.Validate)
cf = file_system.MultiCopyFile([[1, 2, 3]], None)
self.assertRaises(file_system.ValidationError, cf.Validate)
cf = file_system.MultiCopyFile([[1, '/tmp/dest.txt']], None)
self.assertRaises(file_system.ValidationError, cf.Validate)
cf = file_system.MultiCopyFile([['/tmp/src.txt', 2]], None)
self.assertRaises(file_system.ValidationError, cf.Validate)
cf = file_system.MultiCopyFile([['/tmp/src1.txt', '/tmp/dest1.txt'],
['/tmp/src2.txt', '/tmp/dest2.txt']], None)
cf.Validate()
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testMkdir(self, build_info):
md = file_system.MkDir(['/stage/subdir1/subdir2'], build_info)
md.Run()
self.assertTrue(file_system.os.path.exists('/stage/subdir1/subdir2'))
# bad path
md = file_system.MkDir([r'/file1.txt'], build_info)
self.assertRaises(file_system.ActionError, md.Run)
# bad args
md = file_system.MkDir([], build_info)
self.assertRaises(file_system.ActionError, md.Run)
def testMkdirValidate(self):
md = file_system.MkDir('String', None)
self.assertRaises(file_system.ValidationError, md.Validate)
md = file_system.MkDir(['/tmp/some/dir', '/tmp/some/other/dir'], None)
self.assertRaises(file_system.ValidationError, md.Validate)
md = file_system.MkDir([1], None)
self.assertRaises(file_system.ValidationError, md.Validate)
md = file_system.MkDir(['/tmp/some/dir'], None)
md.Validate()
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testSetupCache(self, build_info):
build_info.Cache.return_value.Path.return_value = '/test/cache/path'
sc = file_system.SetupCache([], build_info)
sc.Run()
self.assertTrue(file_system.os.path.exists('/test/cache/path'))
def testSetupCacheValidate(self):
sc = file_system.SetupCache('String', None)
self.assertRaises(file_system.ValidationError, sc.Validate)
sc = file_system.SetupCache([], None)
sc.Validate()
if __name__ == '__main__':
unittest.main()

147
lib/actions/files.py Normal file
View File

@@ -0,0 +1,147 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for interacting with files (text, zip, exe, etc)."""
import logging
import subprocess
import time
import zipfile
from glazier.lib import cache
from glazier.lib import download
from glazier.lib import file_util
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import RestartEvent
from glazier.lib.actions.base import ValidationError
class Execute(BaseAction):
"""Run an executable."""
def _Run(self, command, success_codes, reboot_codes, restart_retry):
result = 0
c = cache.Cache()
logging.debug('Interpreting command %s', command)
try:
command = c.CacheFromLine(command, self._build_info)
except cache.CacheError as e:
raise ActionError(e)
logging.info('Executing command %s', command)
try:
result = subprocess.call(command, shell=True)
except WindowsError as e: # pylint: disable=undefined-variable
raise ActionError('Failed to execute command %s (%s)' % (command, str(e)))
except KeyboardInterrupt:
logging.debug('Child received KeyboardInterrupt. Ignoring.')
if result in reboot_codes:
raise RestartEvent(
'Restart triggered by exit code %d' % result,
5,
retry_on_restart=restart_retry)
elif result not in success_codes:
raise ActionError('Command returned invalid exit code %d' % result)
time.sleep(5)
def Run(self):
for cmd in self._args:
command = cmd[0]
success_codes = [0]
reboot_codes = []
restart_retry = False
if len(cmd) > 1 and cmd[1]:
success_codes = cmd[1]
if len(cmd) > 2 and cmd[2]:
reboot_codes = cmd[2]
if len(cmd) > 3:
restart_retry = cmd[3]
self._Run(command, success_codes, reboot_codes, restart_retry)
def Validate(self):
self._TypeValidator(self._args, list)
for cmd_arg in self._args:
self._TypeValidator(cmd_arg, list)
if not 1 <= len(cmd_arg) <= 4:
raise ValidationError('Invalid args length: %s' % cmd_arg)
self._TypeValidator(cmd_arg[0], str) # cmd
if len(cmd_arg) > 1: # success codes
self._TypeValidator(cmd_arg[1], list)
for arg in cmd_arg[1]:
self._TypeValidator(arg, int)
if len(cmd_arg) > 2: # reboot codes
self._TypeValidator(cmd_arg[2], list)
for arg in cmd_arg[2]:
self._TypeValidator(arg, int)
if len(cmd_arg) > 3: # retry on restart
self._TypeValidator(cmd_arg[3], bool)
class Get(BaseAction):
"""Download a file from a remote source."""
def Run(self):
downloader = download.Download()
for arg in self._args:
src = arg[0]
dst = arg[1]
full_url = download.Transform(src, self._build_info)
if 'https' not in full_url: # support untagged short filenames
full_url = download.PathCompile(self._build_info, file_name=full_url)
try:
file_util.CreateDirectories(dst)
except file_util.Error as e:
raise ActionError('Could not create destination directory %s. %s' %
(dst, e))
try:
downloader.DownloadFile(full_url, dst, show_progress=True)
except download.DownloadError as e:
downloader.PrintDebugInfo()
raise ActionError('Transfer error while downloading %s: %s' %
(full_url, str(e)))
if len(arg) > 2 and arg[2]:
logging.info('Verifying SHA256 hash for %s.', dst)
hash_ok = downloader.VerifyShaHash(dst, arg[2])
if not hash_ok:
raise ActionError('SHA256 hash for %s was incorrect.' % dst)
def Validate(self):
self._TypeValidator(self._args, list)
for arg in self._args:
self._ListOfStringsValidator(arg, 2, 3)
class Unzip(BaseAction):
"""Unzip a zip archive to the local filesystem."""
def Run(self):
try:
zip_file = self._args[0]
out_path = self._args[1]
except IndexError:
raise ActionError('Unable to determine desired paths from %s.' %
str(self._args))
try:
file_util.CreateDirectories(out_path)
except file_util.Error:
raise ActionError('Unable to create output path %s.' % out_path)
try:
zf = zipfile.ZipFile(zip_file)
zf.extractall(out_path)
except (IOError, zipfile.BadZipfile) as e:
raise ActionError('Bad zip file given as input. %s' % e)
def Validate(self):
self._ListOfStringsValidator(self._args, 2)

216
lib/actions/files_test.py Normal file
View File

@@ -0,0 +1,216 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.files."""
from fakefs import fake_filesystem
from fakefs import fake_filesystem_shutil
from glazier.lib import buildinfo
from glazier.lib.actions import files
import mock
import unittest
class FilesTest(unittest.TestCase):
def setUp(self):
self.filesystem = fake_filesystem.FakeFilesystem()
files.open = fake_filesystem.FakeFileOpen(self.filesystem)
files.file_util.shutil = fake_filesystem_shutil.FakeShutilModule(
self.filesystem)
@mock.patch.object(files.time, 'sleep', autospec=True)
@mock.patch.object(files.cache.Cache, 'CacheFromLine', autospec=True)
@mock.patch.object(files.subprocess, 'call', autospec=True)
def testExecute(self, call, cache, sleep):
bi = buildinfo.BuildInfo()
cache.side_effect = iter(['cmd.exe /c', 'explorer.exe'])
e = files.Execute([['cmd.exe /c', [0]], ['explorer.exe']], bi)
call.return_value = 0
e.Run()
call.assert_has_calls([mock.call(
'cmd.exe /c', shell=True), mock.call(
'explorer.exe', shell=True)])
self.assertTrue(sleep.called)
# success codes
cache.side_effect = None
cache.return_value = 'cmd.exe /c script.bat'
e = files.Execute([['cmd.exe /c script.bat', [2, 4]]], bi)
self.assertRaises(files.ActionError, e.Run)
call.return_value = 4
e.Run()
# reboot codes - no retry
e = files.Execute([['cmd.exe /c script.bat', [0], [2, 4]]], bi)
with self.assertRaises(files.RestartEvent) as r_evt:
e.Run()
self.assertEqual(r_evt.retry_on_restart, False)
# reboot codes - retry
e = files.Execute([['cmd.exe /c #script.bat', [0], [2, 4], True]], bi)
with self.assertRaises(files.RestartEvent) as r_evt:
e.Run()
self.assertEqual(r_evt.retry_on_restart, True)
cache.assert_called_with(mock.ANY, 'cmd.exe /c #script.bat', bi)
call.assert_called_with('cmd.exe /c script.bat', shell=True)
# WindowsError
files.WindowsError = Exception
call.side_effect = files.WindowsError
self.assertRaises(files.ActionError, e.Run)
# KeyboardInterrupt
call.side_effect = KeyboardInterrupt
e = files.Execute([['cmd.exe /c', [0]], ['explorer.exe']], bi)
e.Run()
# Cache error
call.side_effect = None
call.return_value = 0
cache.side_effect = files.cache.CacheError
self.assertRaises(files.ActionError, e.Run)
def testExecuteValidation(self):
e = files.Execute([['cmd.exe', [0], [2], False], ['explorer.exe']], None)
e.Validate()
e = files.Execute([[]], None)
self.assertRaises(files.ValidationError, e.Validate)
e = files.Execute(['explorer.exe'], None)
self.assertRaises(files.ValidationError, e.Validate)
e = files.Execute('explorer.exe', None)
self.assertRaises(files.ValidationError, e.Validate)
e = files.Execute([['cmd.exe', [0]], ['explorer.exe', '0']], None)
self.assertRaises(files.ValidationError, e.Validate)
e = files.Execute([['cmd.exe', [0]], ['explorer.exe', ['0']]], None)
self.assertRaises(files.ValidationError, e.Validate)
e = files.Execute([['cmd.exe', [0], ['2']], ['explorer.exe']], None)
self.assertRaises(files.ValidationError, e.Validate)
e = files.Execute([['cmd.exe', [0], [2], 'True'], ['explorer.exe']], None)
self.assertRaises(files.ValidationError, e.Validate)
@mock.patch.object(buildinfo.BuildInfo, 'ReleasePath', autospec=True)
@mock.patch.object(files.download.Download, 'DownloadFile', autospec=True)
@mock.patch.object(files.download.Download, 'VerifyShaHash', autospec=True)
def testGet(self, verify, down_file, r_path):
bi = buildinfo.BuildInfo()
r_path.return_value = 'https://glazier-server.example.com/'
remote = '@glazier/1.0/autobuild.par'
local = r'/tmp/autobuild.par'
test_sha256 = (
'58157bf41ce54731c0577f801035d47ec20ed16a954f10c29359b8adedcae800')
self.filesystem.CreateFile(
r'/tmp/autobuild.par.sha256', contents=test_sha256)
down_file.return_value = True
conf = [[remote, local]]
g = files.Get(conf, bi)
g.Run()
down_file.assert_called_with(
mock.ANY,
'https://glazier-server.example.com/bin/glazier/1.0/autobuild.par',
local,
show_progress=True)
# Relative Paths
conf = [['autobuild.bat', '/tmp/autobuild.bat']]
g = files.Get(conf, bi)
g.Run()
down_file.assert_called_with(
mock.ANY,
'https://glazier-server.example.com/autobuild.bat',
'/tmp/autobuild.bat',
show_progress=True)
down_file.return_value = None
# DownloadError
err = files.download.DownloadError('Error')
down_file.side_effect = err
g = files.Get([[remote, local]], bi)
self.assertRaises(files.ActionError, g.Run)
down_file.side_effect = None
# file_util.Error
self.filesystem.CreateFile('/directory')
g = files.Get([[remote, '/directory/file.txt']], bi)
self.assertRaises(files.ActionError, g.Run)
# good hash
verify.return_value = True
g = files.Get([[remote, local, test_sha256]], bi)
g.Run()
verify.assert_called_with(mock.ANY, local, test_sha256)
# bad hash
verify.return_value = False
g = files.Get([[remote, local, test_sha256]], bi)
self.assertRaises(files.ActionError, g.Run)
# none hash
verify.reset_mock()
conf = [[remote, local, '']]
g = files.Get(conf, bi)
g.Run()
self.assertFalse(verify.called)
def testGetValidate(self):
g = files.Get('String', None)
self.assertRaises(files.ValidationError, g.Validate)
g = files.Get([[1, 2, 3]], None)
self.assertRaises(files.ValidationError, g.Validate)
g = files.Get([[1, '/tmp/out/path']], None)
self.assertRaises(files.ValidationError, g.Validate)
g = files.Get([['/tmp/src.zip', 2]], None)
self.assertRaises(files.ValidationError, g.Validate)
g = files.Get([['https://glazier/bin/src.zip', '/tmp/out/src.zip']], None)
g.Validate()
g = files.Get(
[['https://glazier/bin/src.zip', '/tmp/out/src.zip', '12345']], None)
g.Validate()
g = files.Get([['https://glazier/bin/src.zip', '/tmp/out/src.zip', '12345',
'67890']], None)
self.assertRaises(files.ValidationError, g.Validate)
@mock.patch.object(files.file_util, 'CreateDirectories', autospec=True)
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testUnzip(self, build_info, create_dir):
src = '/tmp/input.zip'
dst = '/out/dir/path'
# bad args
un = files.Unzip([], build_info)
self.assertRaises(files.ActionError, un.Run)
un = files.Unzip([src], build_info)
self.assertRaises(files.ActionError, un.Run)
# bad path
un = files.Unzip([src, dst], build_info)
self.assertRaises(files.ActionError, un.Run)
# create error
create_dir.side_effect = files.file_util.Error
self.assertRaises(files.ActionError, un.Run)
# good
create_dir.side_effect = None
with mock.patch.object(files.zipfile, 'ZipFile', autospec=True) as z:
un = files.Unzip([src, dst], build_info)
un.Run()
z.assert_called_with(src)
z.return_value.extractall.assert_called_with(dst)
create_dir.assert_called_with(dst)
def testUnzipValidate(self):
un = files.Unzip('String', None)
self.assertRaises(files.ValidationError, un.Validate)
un = files.Unzip([1, 2, 3], None)
self.assertRaises(files.ValidationError, un.Validate)
un = files.Unzip([1, '/tmp/out/path'], None)
self.assertRaises(files.ValidationError, un.Validate)
un = files.Unzip(['/tmp/src.zip', 2], None)
self.assertRaises(files.ValidationError, un.Validate)
un = files.Unzip(['/tmp/src.zip', '/tmp/out/path'], None)
un.Validate()
if __name__ == '__main__':
unittest.main()

172
lib/actions/installer.py Normal file
View File

@@ -0,0 +1,172 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for managing the installer."""
import logging
import os
import time
from glazier.chooser import chooser
from glazier.lib import constants
from glazier.lib import log_copy
from glazier.lib.actions import file_system
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import RestartEvent
from glazier.lib.actions.base import ValidationError
from gwinpy.registry import registry
import yaml
class AddChoice(BaseAction):
"""Add a pending question for display in the UI."""
def _Setup(self):
self._realtime = True
def Run(self):
self._build_info.AddChooserOption(self._args)
def Validate(self):
choice = self._args
self._TypeValidator(choice, dict)
for f in ['name', 'type', 'prompt', 'options']:
if f not in choice:
raise ValidationError('Missing required field %s: %s' % (f, choice))
for f in ['name', 'type', 'prompt']:
self._TypeValidator(choice[f], str)
self._TypeValidator(choice['options'], list)
for opt in choice['options']:
self._TypeValidator(opt, dict)
if 'label' not in opt:
raise ValidationError('Missing required field %s: %s' % ('label', opt))
self._TypeValidator(opt['label'], str)
if 'value' not in opt:
raise ValidationError('Missing required field %s: %s' % ('value', opt))
self._TypeValidator(opt['value'], (bool, str))
if 'tip' in opt:
self._TypeValidator(opt['tip'], str)
if 'default' in opt:
self._TypeValidator(opt['default'], bool)
class BuildInfoDump(BaseAction):
"""Dump build information to disk."""
def Run(self):
path = os.path.join(self._build_info.Cache().Path(), 'build_info.yaml')
self._build_info.Serialize(path)
class BuildInfoSave(BaseAction):
"""Save build information to the registry."""
def _WriteRegistry(self, input_keys):
"""Populates the registry with build_info settings for future reference.
Args:
input_keys: A dictionary of key/value pairs to be added to the registry.
"""
reg = registry.Registry(root_key='HKLM')
reg_root = constants.REG_ROOT
for registry_key in input_keys:
registry_value = input_keys[registry_key]
reg.SetKeyValue(reg_root, registry_key, registry_value)
logging.debug('Created registry value named %s with value %s.',
registry_key, registry_value)
def Run(self):
path = os.path.join(self._build_info.Cache().Path(), 'build_info.yaml')
if os.path.exists(path):
with open(path) as handle:
input_config = yaml.safe_load(handle)
self._WriteRegistry(input_config['BUILD'])
os.remove(path)
else:
logging.debug('%s does not exist - skipping processing.', path)
class ExitWinPE(BaseAction):
"""Exit the WinPE environment to start host configuration."""
def Run(self):
cp = file_system.CopyFile([constants.WINPE_TASK_LIST,
constants.SYS_TASK_LIST], self._build_info)
cp.Run()
cp = file_system.CopyFile([constants.WINPE_BUILD_LOG,
constants.SYS_BUILD_LOG], self._build_info)
cp.Run()
raise RestartEvent(
'Leaving WinPE', timeout=10, task_list_path=constants.SYS_TASK_LIST)
class LogCopy(BaseAction):
"""Upload build logs for collection."""
def Run(self):
file_name = str(self._args[0])
share = None
if len(self._args) > 1:
share = str(self._args[1])
logging.debug('Found log copy event for file %s to %s.', file_name, share)
copier = log_copy.LogCopy()
# EventLog
try:
copier.EventLogCopy(file_name)
except log_copy.LogCopyError as e:
logging.warning('Unable to complete log copy to EventLog. %s', e)
# CIFS
if share:
try:
copier.ShareCopy(file_name, share)
except log_copy.LogCopyError as e:
logging.warning('Unable to complete log copy via CIFS. %s', e)
def Validate(self):
self._ListOfStringsValidator(self._args, 1, 2)
class ShowChooser(BaseAction):
"""Show the Chooser UI."""
def Run(self):
ui = chooser.Chooser(options=self._build_info.GetChooserOptions())
ui.Display()
responses = ui.Responses()
self._build_info.StoreChooserResponses(responses)
self._build_info.FlushChooserOptions()
def _Setup(self):
self._realtime = True
class Sleep(BaseAction):
"""Pause the installer."""
def Run(self):
duration = int(self._args[0])
logging.debug('Sleeping for %d seconds.', duration)
time.sleep(duration)
def Validate(self):
self._TypeValidator(self._args, list)
if len(self._args) is not 1:
raise ValidationError('Invalid args length: %s' % self._args)
self._TypeValidator(self._args[0], int)

View File

@@ -0,0 +1,209 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.installer."""
from fakefs import fake_filesystem
from glazier.lib.actions import installer
import mock
import unittest
class InstallerTest(unittest.TestCase):
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testAddChoice(self, build_info):
choice = {
'type':
'toggle',
'prompt':
'Set system shell to PowerShell',
'name':
'core_ps_shell',
'options': [{
'tip': '',
'value': False,
'label': 'False'
}, {
'default': True,
'tip': '',
'value': True,
'label': 'True'
}]
}
a = installer.AddChoice(choice, build_info)
a.Run()
build_info.AddChooserOption.assert_called_with(choice)
def testAddChoiceValidate(self):
choice = {
'type':
'toggle',
'prompt':
'Set system shell to PowerShell',
'name':
'core_ps_shell',
'options': [{
'tip': '',
'value': False,
'label': 'False'
}, {
'default': True,
'tip': '',
'value': True,
'label': 'True'
}]
}
a = installer.AddChoice(choice, None)
a.Validate()
# prompt (name, type)
choice['name'] = True
self.assertRaises(installer.ValidationError, a.Validate)
# tip
choice['name'] = 'core_ps_shell'
choice['options'][0]['tip'] = True
self.assertRaises(installer.ValidationError, a.Validate)
# default
choice['options'][0]['tip'] = ''
choice['options'][0]['default'] = 3
self.assertRaises(installer.ValidationError, a.Validate)
# label
choice['options'][0]['default'] = True
choice['options'][0]['label'] = False
self.assertRaises(installer.ValidationError, a.Validate)
# value
choice['options'][0]['label'] = 'False'
choice['options'][0]['value'] = []
self.assertRaises(installer.ValidationError, a.Validate)
# options dict
choice['options'][0] = False
self.assertRaises(installer.ValidationError, a.Validate)
# options list
choice['options'] = False
self.assertRaises(installer.ValidationError, a.Validate)
del choice['name']
self.assertRaises(installer.ValidationError, a.Validate)
a = installer.AddChoice(False, None)
self.assertRaises(installer.ValidationError, a.Validate)
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testBuildInfoDump(self, build_info):
build_info.Cache.return_value.Path.return_value = r'C:\Cache\Dir'
d = installer.BuildInfoDump(None, build_info)
d.Run()
build_info.Serialize.assert_called_with(r'C:\Cache\Dir/build_info.yaml')
@mock.patch.object(installer.registry, 'Registry', autospec=True)
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testBuildInfoSave(self, build_info, reg):
fs = fake_filesystem.FakeFilesystem()
installer.open = fake_filesystem.FakeFileOpen(fs)
installer.os = fake_filesystem.FakeOsModule(fs)
fs.CreateFile(
'/tmp/build_info.yaml',
contents='{BUILD: {opt 1: true, opt 2: some value, opt 3: 12345}}\n')
build_info.Cache.return_value.Path.return_value = '/tmp'
s = installer.BuildInfoSave(None, build_info)
s.Run()
reg.return_value.SetKeyValue.assert_has_calls(
[
mock.call(installer.constants.REG_ROOT, 'opt 1', True),
mock.call(installer.constants.REG_ROOT, 'opt 2', 'some value'),
mock.call(installer.constants.REG_ROOT, 'opt 3', 12345),
],
any_order=True)
s.Run()
@mock.patch.object(installer.file_system, 'CopyFile', autospec=True)
def testExitWinPE(self, copy):
cache = installer.constants.SYS_CACHE
ex = installer.ExitWinPE(None, None)
with self.assertRaises(installer.RestartEvent):
ex.Run()
copy.assert_has_calls([
mock.call([r'X:\task_list.yaml', '%s\\task_list.yaml' % cache],
mock.ANY),
mock.call().Run(),
])
@mock.patch.object(installer.log_copy, 'LogCopy', autospec=True)
def testLogCopy(self, copy):
log_file = r'X:\glazier.log'
log_host = 'log-server.example.com'
# copy eventlog
lc = installer.LogCopy([log_file], None)
lc.Run()
copy.return_value.EventLogCopy.assert_called_with(log_file)
self.assertFalse(copy.return_value.ShareCopy.called)
copy.reset_mock()
# copy both
lc = installer.LogCopy([log_file, log_host], None)
lc.Run()
copy.return_value.EventLogCopy.assert_called_with(log_file)
copy.return_value.ShareCopy.assert_called_with(log_file, log_host)
copy.reset_mock()
# copy errors
copy.return_value.EventLogCopy.side_effect = installer.log_copy.LogCopyError
copy.return_value.ShareCopy.side_effect = installer.log_copy.LogCopyError
lc.Run()
copy.return_value.EventLogCopy.assert_called_with(log_file)
copy.return_value.ShareCopy.assert_called_with(log_file, log_host)
def testLogCopyValidate(self):
log_host = 'log-server.example.com'
lc = installer.LogCopy(r'X:\glazier.log', None)
self.assertRaises(installer.ValidationError, lc.Validate)
lc = installer.LogCopy([1, 2, 3], None)
self.assertRaises(installer.ValidationError, lc.Validate)
lc = installer.LogCopy([1], None)
self.assertRaises(installer.ValidationError, lc.Validate)
lc = installer.LogCopy([r'X:\glazier.log'], None)
lc.Validate()
lc = installer.LogCopy([r'X:\glazier.log', log_host], None)
lc.Validate()
@mock.patch.object(installer.time, 'sleep', autospec=True)
def testSleep(self, sleep):
s = installer.Sleep([30], None)
s.Run()
sleep.assert_called_with(30)
def testSleepValidate(self):
s = installer.Sleep('30', None)
self.assertRaises(installer.ValidationError, s.Validate)
s = installer.Sleep([1, 2, 3], None)
self.assertRaises(installer.ValidationError, s.Validate)
s = installer.Sleep(['30'], None)
self.assertRaises(installer.ValidationError, s.Validate)
s = installer.Sleep([30], None)
s.Validate()
@mock.patch.object(installer.chooser, 'Chooser', autospec=True)
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testShowChooser(self, build_info, chooser):
c = installer.ShowChooser(None, build_info)
c.Run()
self.assertTrue(chooser.return_value.Display.called)
self.assertTrue(chooser.return_value.Display.called)
build_info.StoreChooserResponses.assert_called_with(
chooser.return_value.Responses.return_value)
self.assertTrue(build_info.FlushChooserOptions.called)
if __name__ == '__main__':
unittest.main()

53
lib/actions/powershell.py Normal file
View File

@@ -0,0 +1,53 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for running Powershell scripts and commands."""
import logging
from glazier.lib import cache
from glazier.lib import powershell
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import ValidationError
class PSScript(BaseAction):
"""Execute a Powershell script file."""
def Run(self):
script = self._args[0]
ps_args = None
if len(self._args) > 1:
ps_args = self._args[1]
ps = powershell.PowerShell(echo_off=True)
c = cache.Cache()
logging.debug('Interpreting Powershell script %s', script)
try:
script = c.CacheFromLine(script, self._build_info)
except cache.CacheError as e:
raise ActionError(e)
try:
ps.RunLocal(script, args=ps_args)
except powershell.PowerShellError as e:
raise ActionError('Failure executing Powershell script. [%s]' % e)
def Validate(self):
self._TypeValidator(self._args, list)
if not 1 <= len(self._args) <= 2:
raise ValidationError('Invalid args length: %s' % self._args)
self._TypeValidator(self._args[0], str)
if len(self._args) > 1:
self._TypeValidator(self._args[1], list)

View File

@@ -0,0 +1,62 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.powershell."""
from glazier.lib import buildinfo
from glazier.lib.actions import powershell
import mock
import unittest
class PowershellTest(unittest.TestCase):
def setUp(self):
buildinfo.constants.FLAGS.config_server = 'https://glazier/'
@mock.patch.object(
powershell.powershell.PowerShell, 'RunLocal', autospec=True)
@mock.patch.object(powershell.cache.Cache, 'CacheFromLine', autospec=True)
def testPSScript(self, cache, run):
bi = buildinfo.BuildInfo()
cache.return_value = r'C:\Cache\Some-Script.ps1'
ps = powershell.PSScript(['#Some-Script.ps1', ['-Flag1']], bi)
ps.Run()
cache.assert_called_with(mock.ANY, '#Some-Script.ps1', bi)
run.assert_called_with(
mock.ANY, r'C:\Cache\Some-Script.ps1', args=['-Flag1'])
run.side_effect = powershell.powershell.PowerShellError
self.assertRaises(powershell.ActionError, ps.Run)
# Cache error
run.side_effect = None
cache.side_effect = powershell.cache.CacheError
self.assertRaises(powershell.ActionError, ps.Run)
def testPSScriptValidate(self):
ps = powershell.PSScript(30, None)
self.assertRaises(powershell.ValidationError, ps.Validate)
ps = powershell.PSScript([], None)
self.assertRaises(powershell.ValidationError, ps.Validate)
ps = powershell.PSScript([30, 40], None)
self.assertRaises(powershell.ValidationError, ps.Validate)
ps = powershell.PSScript(['#Some-Script.ps1'], None)
ps.Validate()
ps = powershell.PSScript(['#Some-Script.ps1', '-Flags'], None)
self.assertRaises(powershell.ValidationError, ps.Validate)
ps = powershell.PSScript(['#Some-Script.ps1', ['-Flags']], None)
ps.Validate()
if __name__ == '__main__':
unittest.main()

41
lib/actions/registry.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for managing the host registry."""
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from gwinpy.registry import registry
class RegAdd(BaseAction):
"""Add a new registry key."""
def Run(self):
use_64bit = True
if len(self._args) > 5:
use_64bit = self._args[5]
try:
reg = registry.Registry(root_key=self._args[0])
reg.SetKeyValue(key_path=self._args[1],
key_name=self._args[2],
key_value=self._args[3],
key_type=self._args[4],
use_64bit=use_64bit)
except registry.RegistryError as e:
raise ActionError(str(e))
except IndexError:
raise ActionError('Unable to access all required arguments. [%s]' %
str(self._args))

View File

@@ -0,0 +1,53 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.registry."""
from glazier.lib.actions import registry
import mock
import unittest
class RegistryTest(unittest.TestCase):
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
@mock.patch.object(registry.registry, 'Registry', autospec=True)
def testRun(self, winreg, build_info):
kpath = (r'SOFTWARE\Microsoft\Windows NT\CurrentVersion'
r'\SoftwareProtectionPlatform')
args = [
'HKLM', kpath, 'KeyManagementServiceName', 'kms-server.example.com',
'REG_SZ', False
]
skv = winreg.return_value.SetKeyValue
ra = registry.RegAdd(args, build_info)
ra.Run()
skv.assert_called_with(
key_path=kpath,
key_name='KeyManagementServiceName',
key_value='kms-server.example.com',
key_type='REG_SZ',
use_64bit=False)
# registry error
skv.side_effect = registry.registry.RegistryError
self.assertRaises(registry.ActionError, ra.Run)
skv.side_effect = None
# missing arguments
ra = registry.RegAdd(args[2:], build_info)
self.assertRaises(registry.ActionError, ra.Run)
if __name__ == '__main__':
unittest.main()

89
lib/actions/sysprep.py Normal file
View File

@@ -0,0 +1,89 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for sysprep-related activites."""
import logging
import re
from glazier.lib import timezone
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from gwinpy.net import dhcp
class SetUnattendTimeZone(BaseAction):
"""Configure the TimeZone entries in unattend.xml."""
def _EditUnattend(self,
zone,
unattend_path=r'C:\Windows\Panther\unattend.xml'):
"""Edit the unattend.xml to replace the timezone entry.
Args:
zone: The timezone string to insert into <TimeZone></TimeZone>
unattend_path: Path to the unattend.xml file.
"""
lines = []
try:
with open(unattend_path) as unattend:
lines = unattend.readlines()
lines = [
re.sub('<TimeZone>.*?</TimeZone>', '<TimeZone>%s</TimeZone>' % zone,
l) for l in lines
]
with open(unattend_path, 'w') as unattend:
unattend.write(''.join(lines))
except IOError as e:
raise ActionError('Unable to set time zone in unattend.xml (%s)' % str(e))
def Run(self):
"""Sets the timezone inside unattend.xml."""
local_tz = 'Pacific Standard Time'
from_dhcp = False
retries = 0
while not from_dhcp and retries < 5:
for intf in self._build_info.NetInterfaces():
if intf.ip_address and intf.mac_address:
servers = ['255.255.255.255']
if intf.dhcp_server:
servers.insert(0, intf.dhcp_server)
logging.debug(
'Attempting to get timezone from interface with IP %s and MAC %s',
intf.ip_address, intf.mac_address)
for dhcp_server in servers:
from_dhcp = dhcp.GetDhcpOption(
client_addr=intf.ip_address,
client_mac=intf.mac_address,
option=101, server_addr=dhcp_server)
logging.debug('DHCP server %s returned: %s', dhcp_server, from_dhcp)
if from_dhcp:
break
if from_dhcp:
break
logging.debug('No result from DHCP. Retrying...')
retries += 1
if from_dhcp:
logging.debug('Got timezone %s from DHCP.', from_dhcp)
tz = timezone.Timezone(load_map=True)
translated = tz.TranslateZone(from_dhcp)
if translated:
local_tz = translated
logging.debug('Successfully translated timezone to %s.', local_tz)
else:
logging.error('Could not translate DHCP timezone.')
else:
logging.error('Could not find timezone from DHCP.')
logging.debug('Finalized timezone is %s.', local_tz)
self._EditUnattend(local_tz)

134
lib/actions/sysprep_test.py Normal file
View File

@@ -0,0 +1,134 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.sysprep."""
from fakefs import fake_filesystem
from glazier.lib.actions import sysprep
import mock
import unittest
UNATTEND_XML = r"""<?xml version='1.0' encoding='utf-8'?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="specialize" wasPassProcessed="true">
<component name="Microsoft-Windows-Shell-Setup">
<RegisteredOrganization>Company</RegisteredOrganization>
<RegisteredOwner>Employee</RegisteredOwner>
<ComputerName>*</ComputerName>
<ShowWindowsLive>false</ShowWindowsLive>
<TimeZone>Central Standard Time</TimeZone>
<CopyProfile>true</CopyProfile>
</component>
</settings>
<settings pass="oobeSystem" wasPassProcessed="true">
<component name="Microsoft-Windows-International-Core">
<InputLocale>en-us</InputLocale>
<SystemLocale>en-us</SystemLocale>
<UILanguage>en-us</UILanguage>
<UserLocale>en-us</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup">
<TimeZone>Central Standard Time</TimeZone>
<LogonCommands>
<AsynchronousCommand wcm:action="add">
<CommandLine>cmd /c C:\prepare_build.bat</CommandLine>
<Description>Prepare build</Description>
<Order>1</Order>
<RequiresUserInput>true</RequiresUserInput>
</AsynchronousCommand>
</LogonCommands>
</component>
</settings>
</unattend>"""
class SysprepTest(unittest.TestCase):
def setUp(self):
fakefs = fake_filesystem.FakeFilesystem()
fakefs.CreateDirectory('/windows/panther')
fakefs.CreateFile('/windows/panther/unattend.xml', contents=UNATTEND_XML)
self.fake_open = fake_filesystem.FakeFileOpen(fakefs)
sysprep.os = fake_filesystem.FakeOsModule(fakefs)
sysprep.open = self.fake_open
self.fakefs = fakefs
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testSetUnattendTimeZoneEditUnattend(self, build_info):
st = sysprep.SetUnattendTimeZone([], build_info)
st._EditUnattend(
'Yakutsk Standard Time', unattend_path='/windows/panther/unattend.xml')
with self.fake_open('/windows/panther/unattend.xml') as handle:
result = [line.strip() for line in handle.readlines()]
self.assertIn('<TimeZone>Yakutsk Standard Time</TimeZone>', result)
# IOError
self.assertRaises(sysprep.ActionError, st._EditUnattend,
'Yakutsk Standard Time',
'/windows/panther/noneattend.xml')
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
@mock.patch.object(
sysprep.SetUnattendTimeZone, '_EditUnattend', autospec=True)
@mock.patch.object(sysprep.dhcp, 'GetDhcpOption', autospec=True)
def testSetUnattendTimeZoneRun(self, dhcp, edit, build_info):
build_info.NetInterfaces.return_value = [
mock.Mock(
ip_address='127.0.0.1',
mac_address='11:22:33:44:55:66',
dhcp_server=None),
mock.Mock(ip_address='127.0.0.2', mac_address=None, dhcp_server=None),
mock.Mock(
ip_address=None, mac_address='22:11:33:44:55:66', dhcp_server=None),
mock.Mock(
ip_address='10.1.10.1',
mac_address='AA:BB:CC:DD:EE:FF',
dhcp_server='192.168.1.1')
]
st = sysprep.SetUnattendTimeZone([], build_info)
# Normal Run
dhcp.side_effect = iter([None, None, 'Antarctica/McMurdo'])
st.Run()
dhcp.assert_has_calls([
mock.call(
client_addr='127.0.0.1',
client_mac='11:22:33:44:55:66',
option=101,
server_addr='255.255.255.255'),
mock.call(
client_addr='10.1.10.1',
client_mac='AA:BB:CC:DD:EE:FF',
option=101,
server_addr='192.168.1.1'),
mock.call(
client_addr='10.1.10.1',
client_mac='AA:BB:CC:DD:EE:FF',
option=101,
server_addr='255.255.255.255')
])
edit.assert_called_with(st, u'New Zealand Standard Time')
# Failed Mapping
dhcp.side_effect = None
dhcp.return_value = 'Antarctica/NorthPole'
st.Run()
edit.assert_called_with(st, u'Pacific Standard Time')
# No Result
dhcp.return_value = None
st.Run()
edit.assert_called_with(st, u'Pacific Standard Time')
if __name__ == '__main__':
unittest.main()

60
lib/actions/system.py Normal file
View File

@@ -0,0 +1,60 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for interacting with the host system."""
import logging
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import RestartEvent
from glazier.lib.actions.base import ShutdownEvent
from glazier.lib.actions.base import ValidationError
class _PowerAction(BaseAction):
def Validate(self):
self._TypeValidator(self._args, list)
if len(self._args) not in [1, 2]:
raise ValidationError('Invalid args length: %s' % self._args)
if not isinstance(self._args[0], str) and not isinstance(self._args[0],
int):
raise ValidationError('Invalid argument type: %s' % self._args[0])
if len(self._args) > 1 and not isinstance(self._args[1], str):
raise ValidationError('Invalid argument type: %s' % self._args[1])
class Reboot(_PowerAction):
"""Perform a host reboot."""
def Run(self):
timeout = str(self._args[0])
reason = 'unspecified'
if len(self._args) > 1:
reason = str(self._args[1])
logging.info('Rebooting with a timeout of %s and a reason of %s', timeout,
reason)
raise RestartEvent(reason, timeout=timeout)
class Shutdown(_PowerAction):
"""Perform a host shutdown."""
def Run(self):
timeout = str(self._args[0])
reason = 'unspecified'
if len(self._args) > 1:
reason = str(self._args[1])
logging.info('Shutting down with a timeout of %s and a reason of %s',
timeout, reason)
raise ShutdownEvent(reason, timeout=timeout)

View File

@@ -0,0 +1,77 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.system."""
from glazier.lib.actions import system
import mock
import unittest
class SystemTest(unittest.TestCase):
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testReboot(self, build_info):
r = system.Reboot([30, 'reboot for reasons'], build_info)
with self.assertRaises(system.RestartEvent) as evt:
r.Run()
self.assertEqual(evt.timeout, '30')
self.assertEqual(evt.message, 'reboot for reasons')
r = system.Reboot([10], build_info)
with self.assertRaises(system.RestartEvent) as evt:
r.Run()
self.assertEqual(evt.timeout, '10')
self.assertEqual(evt.message, 'undefined')
def testRebootValidate(self):
r = system.Reboot(30, None)
self.assertRaises(system.ValidationError, r.Validate)
r = system.Reboot([], None)
self.assertRaises(system.ValidationError, r.Validate)
r = system.Reboot([30, 40], None)
self.assertRaises(system.ValidationError, r.Validate)
r = system.Reboot([30, 'reasons'], None)
r.Validate()
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testShutdown(self, build_info):
r = system.Shutdown([15, 'reboot for reasons'], build_info)
with self.assertRaises(system.ShutdownEvent) as evt:
r.Run()
self.assertEqual(evt.timeout, '15')
self.assertEqual(evt.message, 'reboot for reasons')
r = system.Shutdown([1], build_info)
with self.assertRaises(system.ShutdownEvent) as evt:
r.Run()
self.assertEqual(evt.timeout, '1')
self.assertEqual(evt.message, 'undefined')
def testShutdownValidate(self):
s = system.Shutdown(30, None)
self.assertRaises(system.ValidationError, s.Validate)
s = system.Shutdown([], None)
self.assertRaises(system.ValidationError, s.Validate)
s = system.Shutdown([30, 40], None)
self.assertRaises(system.ValidationError, s.Validate)
s = system.Shutdown([30, 'reasons'], None)
s.Validate()
s = system.Shutdown([10], None)
s.Validate()
if __name__ == '__main__':
unittest.main()

27
lib/actions/timers.py Normal file
View File

@@ -0,0 +1,27 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions to set imaging timers."""
from glazier.lib.actions.base import BaseAction
class SetTimer(BaseAction):
"""Create an imaging timer."""
def Run(self):
self._build_info.TimerSet(str(self._args[0]))
def Validate(self):
self._ListOfStringsValidator(self._args)

View File

@@ -0,0 +1,45 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.timers."""
from glazier.lib.actions import timers
from glazier.lib.actions.base import ValidationError
import mock
import unittest
class TimersTest(unittest.TestCase):
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testSetTimer(self, build_info):
args = ['Timer1']
st = timers.SetTimer(args, build_info)
st.Run()
build_info.TimerSet.assert_called_with('Timer1')
def testSetTimerValidate(self):
st = timers.SetTimer('Timer1', None)
self.assertRaises(ValidationError, st.Validate)
st = timers.SetTimer([1, 2, 3], None)
self.assertRaises(ValidationError, st.Validate)
st = timers.SetTimer([1], None)
self.assertRaises(ValidationError, st.Validate)
st = timers.SetTimer(['Timer1'], None)
st.Validate()
if __name__ == '__main__':
unittest.main()

38
lib/actions/tpm.py Normal file
View File

@@ -0,0 +1,38 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions to manage the system TPM."""
from glazier.lib import bitlocker
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import ValidationError
class BitlockerEnable(BaseAction):
def Run(self):
mode = str(self._args[0])
try:
bl = bitlocker.Bitlocker(mode)
bl.Enable()
except bitlocker.BitlockerError as e:
raise ActionError('Failure enabling Bitlocker. (%s)' % str(e))
def Validate(self):
self._ListOfStringsValidator(self._args, 1)
if self._args[0] not in bitlocker.SUPPORTED_MODES:
raise ValidationError('Unknown mode for BitlockerEnable: %s' %
self._args[0])

48
lib/actions/tpm_test.py Normal file
View File

@@ -0,0 +1,48 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.tpm."""
from glazier.lib.actions import tpm
import mock
import unittest
class TpmTest(unittest.TestCase):
@mock.patch.object(tpm.bitlocker, 'Bitlocker', autospec=True)
def testBitlockerEnable(self, bitlocker):
b = tpm.BitlockerEnable(['ps_tpm'], None)
b.Run()
bitlocker.assert_called_with('ps_tpm')
self.assertTrue(bitlocker.return_value.Enable.called)
bitlocker.return_value.Enable.side_effect = tpm.bitlocker.BitlockerError
self.assertRaises(tpm.ActionError, b.Run)
def testBitlockerEnableValidate(self):
b = tpm.BitlockerEnable(30, None)
self.assertRaises(tpm.ValidationError, b.Validate)
b = tpm.BitlockerEnable([], None)
self.assertRaises(tpm.ValidationError, b.Validate)
b = tpm.BitlockerEnable(['invalid'], None)
self.assertRaises(tpm.ValidationError, b.Validate)
b = tpm.BitlockerEnable(['ps_tpm', 'ps_tpm'], None)
self.assertRaises(tpm.ValidationError, b.Validate)
b = tpm.BitlockerEnable(['ps_tpm'], None)
b.Validate()
if __name__ == '__main__':
unittest.main()

104
lib/actions/updates.py Normal file
View File

@@ -0,0 +1,104 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Actions for managing updates for specific machines."""
import logging
import os
from glazier.lib import constants
from glazier.lib import file_util
from glazier.lib.actions.base import ActionError
from glazier.lib.actions.base import BaseAction
from glazier.lib.actions.base import ValidationError
from glazier.lib.actions.files import Execute
from glazier.lib.actions.files import Get
class UpdateMSU(BaseAction):
"""Downloads file and verifies extension.
File is downloaded and processed based on supported file extension.
file can then be processed to be used by dism commands.
Method can be expanded to access and process other formats allowed by DISM.
Also can be used to process multiple files.
Raises:
ActionError: Call with unsupported file type.
"""
FILE_EXT_SUPPORTED = ['.msu']
def Run(self):
for msu in self._args:
dst = str(msu[1])
file_ext = os.path.splitext(dst)[1]
if file_ext not in self.FILE_EXT_SUPPORTED:
raise ActionError('Unsupported update file format %s.' % dst)
g = Get([msu], self._build_info)
g.Run()
logging.info('Found MSU file, processing update using DISM.')
self._ProcessMsu(dst)
def Validate(self):
self._TypeValidator(self._args, list)
for cmd_arg in self._args:
self._TypeValidator(cmd_arg, list)
if not 2 <= len(cmd_arg) <= 3:
raise ValidationError('Invalid args length: %s' % cmd_arg)
self._TypeValidator(cmd_arg[0], str) # remote
self._TypeValidator(cmd_arg[1], str) # local
file_ext = os.path.splitext(cmd_arg[1])[1]
if file_ext not in self.FILE_EXT_SUPPORTED:
raise ValidationError('Invalid file type: %s' % cmd_arg[1])
if len(cmd_arg) > 2: # hash
for arg in cmd_arg[2]:
self._TypeValidator(arg, str)
def _ProcessMsu(self, msu_file):
"""Command used to process updates downloaded.
This command will apply updates to an image.
If the exit code for the parsed command is anything other than zero, report
fatal error.
Args:
msu_file: current file location.
Raises:
ActionError: Error during update application.
"""
scratch_dir = '%s\\Updates\\' % constants.SYS_CACHE
# create scratch directory
file_util.CreateDirectories(scratch_dir)
# dism commands
update = [
'{} /image:c:\\ /Add-Package /PackagePath:{} /ScratchDir:{}'.format(
constants.WINPE_DISM, msu_file, scratch_dir)
]
logging.info('Applying %s image to main disk.', msu_file)
# Apply updates to image
ex = Execute([update], self._build_info)
try:
ex.Run()
except ActionError as e:
raise ActionError('Unable to process update %s. (%s)' % (msu_file, e))

View File

@@ -0,0 +1,92 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.actions.updates."""
from glazier.lib.actions import updates
from glazier.lib.buildinfo import BuildInfo
import mock
import unittest
class UpdatesTest(unittest.TestCase):
@mock.patch.object(BuildInfo, 'ReleasePath')
@mock.patch('glazier.lib.download.Download.VerifyShaHash', autospec=True)
@mock.patch('glazier.lib.download.Download.DownloadFile', autospec=True)
@mock.patch.object(updates, 'Execute', autospec=True)
@mock.patch.object(updates.file_util, 'CreateDirectories', autospec=True)
def testUpdateMSU(self, mkdir, exe, dl, sha, rpath):
bi = BuildInfo()
# Setup
remote = '@Drivers/HP/KB2990941-v3-x64.msu'
local = r'c:\KB2990941-v3-x64.msu'
sha_256 = (
'd1acbdd8652d6c78ce284bf511f3a7f5f776a0a91357aca060039a99c6a93a16')
conf = {'data': {'update': [[remote, local, sha_256]]},
'path': ['/autobuild']}
rpath.return_value = '/'
# Success
um = updates.UpdateMSU(conf['data']['update'], bi)
um.Run()
dl.assert_called_with(
mock.ANY, ('https://glazier-server.example.com/'
'bin/Drivers/HP/KB2990941-v3-x64.msu'),
local,
show_progress=True)
sha.assert_called_with(mock.ANY, local, sha_256)
cache = updates.constants.SYS_CACHE
exe.assert_called_with([[(
'X:\\Windows\\System32\\dism.exe /image:c:\\ '
'/Add-Package /PackagePath:c:\\KB2990941-v3-x64.msu '
'/ScratchDir:%s\\Updates\\' % cache)]], mock.ANY)
mkdir.assert_called_with('%s\\Updates\\' % cache)
# Invalid format
conf['data']['update'][0][1] = 'C:\\Windows6.1-KB2990941-v3-x64.cab'
um = updates.UpdateMSU(conf['data']['update'], bi)
self.assertRaises(updates.ActionError, um.Run)
conf['data']['update'][0][1] = 'C:\\Windows6.1-KB2990941-v3-x64.msu'
# Dism Fail
exe.return_value.Run.side_effect = updates.ActionError()
self.assertRaises(updates.ActionError, um.Run)
def testUpdateMSUValidate(self):
g = updates.UpdateMSU('String', None)
self.assertRaises(updates.ValidationError, g.Validate)
g = updates.UpdateMSU([[1, 2, 3]], None)
self.assertRaises(updates.ValidationError, g.Validate)
g = updates.UpdateMSU([[1, '/tmp/out/path']], None)
self.assertRaises(updates.ValidationError, g.Validate)
g = updates.UpdateMSU([['/tmp/src.zip', 2]], None)
self.assertRaises(updates.ValidationError, g.Validate)
g = updates.UpdateMSU(
[['https://glazier/bin/src.msu', '/tmp/out/src.zip']], None)
self.assertRaises(updates.ValidationError, g.Validate)
g = updates.UpdateMSU(
[['https://glazier/bin/src.msu', '/tmp/out/src.msu']], None)
g.Validate()
g = updates.UpdateMSU(
[['https://glazier/bin/src.msu', '/tmp/out/src.msu', '12345']], None)
g.Validate()
g = updates.UpdateMSU([['https://glazier/bin/src.zip', '/tmp/out/src.zip',
'12345', '67890']], None)
self.assertRaises(updates.ValidationError, g.Validate)
if __name__ == '__main__':
unittest.main()

74
lib/bitlocker.py Normal file
View File

@@ -0,0 +1,74 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Bitlocker management functionality."""
import logging
import subprocess
from glazier.lib import constants
from glazier.lib import powershell
SUPPORTED_MODES = ['ps_tpm', 'bde_tpm']
class BitlockerError(Exception):
pass
class Bitlocker(object):
"""Manage Bitlocker related operations on the local host."""
def __init__(self, mode):
self._mode = mode
def _LaunchSubproc(self, command):
"""Launch a subprocess.
Args:
command: A command string to pass to subprocess.call()
Raises:
BitlockerError: An unexpected exit code from manage-bde.
"""
logging.info('Running BitLocker command: %s', command)
exit_code = subprocess.call(command, shell=True)
if exit_code != 0:
raise BitlockerError('Unexpected exit code from Bitlocker: %s.' %
str(exit_code))
def _PsTpm(self):
"""Enable TPM mode using Powershell (Win8 +)."""
ps = powershell.PowerShell()
try:
ps.RunCommand(['$ErrorActionPreference=\'Stop\'', ';', 'Enable-BitLocker',
'C:', '-TpmProtector', '-UsedSpaceOnly',
'-SkipHardwareTest ', '>>',
r'%s\enable-bitlocker.txt' % constants.SYS_LOGS_PATH])
ps.RunCommand(['$ErrorActionPreference=\'Stop\'', ';',
'Add-BitLockerKeyProtector', 'C:',
'-RecoveryPasswordProtector', '>NUL'])
except powershell.PowerShellError as e:
raise BitlockerError('Error enabling Bitlocker via Powershell: %s.' %
str(e))
def Enable(self):
"""Enable bitlocker."""
if self._mode == 'ps_tpm':
self._PsTpm()
elif self._mode == 'bde_tpm':
self._LaunchSubproc(r'C:\Windows\System32\cmd.exe /c '
r'C:\Windows\System32\manage-bde.exe -on c: -rp '
'>NUL')
else:
raise BitlockerError('Unknown mode: %s.' % self._mode)

59
lib/bitlocker_test.py Normal file
View File

@@ -0,0 +1,59 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.bitlocker."""
from glazier.lib import bitlocker
import mock
import unittest
class BitlockerTest(unittest.TestCase):
@mock.patch.object(
bitlocker.powershell.PowerShell, 'RunCommand', autospec=True)
def testPowershell(self, ps):
bit = bitlocker.Bitlocker(mode='ps_tpm')
bit.Enable()
ps.assert_has_calls([
mock.call(mock.ANY, [
"$ErrorActionPreference='Stop'", ';', 'Enable-BitLocker', 'C:',
'-TpmProtector', '-UsedSpaceOnly', '-SkipHardwareTest ', '>>',
'%s\\enable-bitlocker.txt' % bitlocker.constants.SYS_LOGS_PATH
]), mock.call(mock.ANY, [
"$ErrorActionPreference='Stop'", ';', 'Add-BitLockerKeyProtector',
'C:', '-RecoveryPasswordProtector', '>NUL'
])
])
ps.side_effect = bitlocker.powershell.PowerShellError
self.assertRaises(bitlocker.BitlockerError, bit.Enable)
@mock.patch.object(bitlocker.subprocess, 'call', autospec=True)
def testManageBde(self, call):
bit = bitlocker.Bitlocker(mode='bde_tpm')
call.return_value = 0
cmdline = ('C:\\Windows\\System32\\cmd.exe /c '
'C:\\Windows\\System32\\manage-bde.exe -on c: -rp >NUL')
bit.Enable()
call.assert_called_with(cmdline, shell=True)
call.return_value = 1
self.assertRaises(bitlocker.BitlockerError, bit.Enable)
def testFailure(self):
bit = bitlocker.Bitlocker(mode='unsupported')
self.assertRaises(bitlocker.BitlockerError, bit.Enable)
if __name__ == '__main__':
unittest.main()

582
lib/buildinfo.py Normal file
View File

@@ -0,0 +1,582 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Glazier host information discovery subsystem."""
import logging
import time
from glazier.lib import cache
from glazier.lib import constants
from glazier.lib import timers
from glazier.lib.config import files
from glazier.lib.spec import spec
from gwinpy.wmi import hw_info
from gwinpy.wmi import net_info
from gwinpy.wmi import tpm_info
import yaml
class BuildInfoError(Exception):
pass
class BuildInfo(object):
"""Encapsulates information pertaining to the build."""
def __init__(self):
self._active_conf_path = []
self._cache = None
self._chooser_pending = []
self._chooser_responses = {}
self._hw_info = None
self._net_info = None
self._release_info = None
self._timers = timers.Timers()
self._tpm_info = None
self._version_info = None
#
# Chooser Control Functions
#
def AddChooserOption(self, option):
"""Add an option to the chooser pending questions list."""
self._chooser_pending.append(option)
def GetChooserOptions(self):
"""Retrieve all pending chooser options."""
return self._chooser_pending
def FlushChooserOptions(self):
"""Clear all pending chooser options."""
self._chooser_pending = []
def StoreChooserResponses(self, responses):
"""Store responses from the Chooser UI."""
for key in responses:
renamed = 'USER_%s' % key
logging.debug('Importing key %s from chooser.', renamed)
self._chooser_responses[renamed] = responses[key]
#
# Image Configuration Functions
#
def BinaryPath(self):
"""Determines the path to the folder containing all binaries for build.
Returns:
The versioned base path to the current build as a string.
"""
server = self.ConfigServer() or ''
path = constants.FLAGS.binary_root_path.strip('/')
path = '%s/%s/' % (server, path)
return path
def ConfigServer(self):
server = constants.FLAGS.config_server
if server:
server = server.rstrip('/')
return server
def Release(self):
"""Determine the current build release.
Returns:
The build release as a string.
"""
rel_id_file = '%s/%s' % (self.ReleasePath().rstrip('/'), 'release-id.yaml')
try:
data = files.Read(rel_id_file)
except files.Error as e:
raise BuildInfoError(e)
if data and 'release_id' in data:
return data['release_id']
return None
def _ReleaseInfo(self):
if not self._release_info:
rel_info_file = '%s/%s' % (self.ReleasePath().rstrip('/'),
'release-info.yaml')
try:
self._release_info = files.Read(rel_info_file)
except files.Error as e:
raise BuildInfoError(e)
return self._release_info
def ReleasePath(self):
"""Determines the path to the folder containing all files for build.
Returns:
The versioned base path to the current build as a string.
"""
path = self.ConfigServer() or ''
if self.Branch():
path += '/%s' % str(self.Branch())
path += '/'
return path
def ActiveConfigPath(self, append=None, pop=False, set_to=None):
"""Tracks the active configuration path beneath the config root.
Use append/pop for directory traversal.
Args:
append: Append a string to the active config path.
pop: Pop the rightmost string from the active config path.
set_to: Set the config path to an entirely new path.
Returns:
The active config path after any modifications.
"""
if append:
self._active_conf_path.append(append)
elif set_to:
self._active_conf_path = set_to
elif pop and self._active_conf_path:
self._active_conf_path.pop()
return self._active_conf_path
def _VersionInfo(self):
if not self._version_info:
info_file = '%s/%s' % (self.ConfigServer().rstrip('/'),
'version-info.yaml')
try:
self._version_info = files.Read(info_file)
except files.Error as e:
raise BuildInfoError(e)
return self._version_info
#
# Host Discovery Functions
#
def BuildPinMatch(self, pin_name, pin_values):
"""Compare build pins to local build info data.
Most pins operate on a simple 1:1 string comparison (eg os_code ==
os_code). Pins also support negation match by beginning the pin value
with ! (!win7 matches anything except win7). See _StringPinner for details.
Special cases:
computer_model: Permits partial string matching.
device_id: Performs a many:many matching, as it's comparing against a
list of all known internal hardware ids instead of just one string.
USER_*: USER_ pins are dynamic, based on options offered by the chooser.
There is no validation on the names of these inputs, as they may
vary between uses. No negation.
Args:
pin_name: The name of the pin (determines function for comparison).
pin_values: A list of all pin values configured for this pin.
Returns:
True for a pin match, else False.
Raises:
BuildInfoError: Reference made to an unsupported pin.
"""
known_pins = self.GetExportedPins()
if pin_name.startswith('USER_'):
if pin_name in self._chooser_responses:
return self._StringPinner([self._chooser_responses[pin_name]],
pin_values)
else:
return False
elif pin_name not in known_pins:
raise BuildInfoError('Referencing illegal pin name: %s' % pin_name)
loose = False
if pin_name in ['computer_model', 'device_id']:
loose = True
values = known_pins[pin_name]()
values = values if isinstance(values, list) else [values]
return self._StringPinner(values, pin_values, loose=loose)
def GetExportedPins(self):
return {
'computer_model': self.ComputerModel,
'device_id': self.DeviceIds,
'encryption_type': self.EncryptionLevel,
'graphics': self.VideoControllersByName,
'os_code': self.OsCode,
}
def Cache(self):
"""The local build cache.
Returns:
An instance of the Cache class.
"""
if not self._cache:
self._cache = cache.Cache()
return self._cache
def ComputerManufacturer(self):
"""Get the computer manufacturer from WMI.
Returns:
A string containing the device manufacturer.
Raises:
BuildInfoError: Failure determining the system manufacturer.
"""
result = self._HWInfo().ComputerSystemManufacturer()
if not result:
raise BuildInfoError('System manufacturer could not be determined.')
return result
def ComputerModel(self):
"""Get the computer model from WMI.
Lenovo models are trimmed to three characters to mitigate submodel drift.
Returns:
the hardware model as a string
Raises:
BuildInfoError: Failure determining the system model.
"""
result = self._HWInfo().ComputerSystemModel()
if not result:
raise BuildInfoError('System model could not be determined.')
return result
def ComputerName(self):
"""Get the assigned computer name string.
Returns:
The name string assigned to this machine.
"""
return spec.GetModule().GetHostname()
def ComputerOs(self):
"""Get the assigned computer OS string.
Returns:
The OS string assigned to this machine.
"""
return spec.GetModule().GetOs()
def ComputerSerial(self):
"""Get the computer serial from WMI.
Returns:
A string containing the computer serial.
"""
return self._HWInfo().BiosSerial()
def DeviceIds(self):
"""Get local hardware device Ids.
Returns:
A list containing all detected hardware device IDs in the format
[vendor]-[device]-[subsys]-[revision]
"""
dev_ids = []
for device in self._HWInfo().PciDevices():
dev_str = '%s-%s-%s-%s' % (device.ven, device.dev, device.subsys,
device.rev)
logging.debug('Found local device: %s', dev_str)
dev_ids.append(dev_str)
return dev_ids
def EncryptionLevel(self):
"""Determines what encryption level is required for this machine.
Returns:
The required encryption type as a string (none, tpm)
"""
if self.IsVirtual():
logging.info('Virtual machine type %s does not require full disk '
'encryption.', self.ComputerModel())
return 'none'
logging.info('Machine %s requires full disk encryption.',
self.ComputerModel())
if self.TpmPresent():
logging.info('TPM detected - using TPM encryption.')
return 'tpm'
logging.info('No TPM was detected in this machine.')
return 'tpm'
def Fqdn(self):
"""Get the assigned FQDN string.
Returns:
The FQDN string assigned to this machine.
"""
return spec.GetModule().GetFqdn()
def _HWInfo(self):
if not self._hw_info:
self._hw_info = hw_info.HWInfo()
return self._hw_info
def IsLaptop(self):
"""Whether or not this machine is a laptop.
Returns:
true for laptop, else false
"""
return self._HWInfo().IsLaptop()
def IsVirtual(self):
"""Whether or not this build is in a virtual environment.
Returns:
true for a virtual build, else false
"""
return self._HWInfo().IsVirtualMachine()
def KnownBranches(self):
return self._VersionInfo()['versions']
def _NetInfo(self):
if not self._net_info:
self._net_info = net_info.NetInfo(active_only=False, poll=True)
return self._net_info
def NetInterfaces(self, active_only=True):
"""Access the local network interfaces.
Args:
active_only: Only consider active interfaces.
Returns:
A list of NetInterface objects corresponding to each detected interface.
"""
ni = net_info.NetInfo(active_only=active_only, poll=True)
return ni.Interfaces()
def OsCode(self):
"""Return the OS code associated with this build.
Returns:
the os code as a string
Raises:
BuildInfoError: Reference to an unknown operating system.
"""
os = self.ComputerOs()
release_info = self._ReleaseInfo()
if 'os_codes' in release_info:
os_codes = release_info['os_codes']
if os in os_codes:
return os_codes[os]['code']
raise BuildInfoError('Unknown OS [%s]', os)
def Serialize(self, to_file):
"""Dumps internal data to a file for later reference."""
build_data = {
'BUILD': {
'Binary Path': str(self.BinaryPath()),
'branch': str(self.Branch()),
'build_timestamp': str(time.strftime('%m/%d/%Y %H:%M:%S')),
'Chassis': str(self._HWInfo().ChassisType()),
'Name': str(self.ComputerName()),
'encryption_type': str(self.EncryptionLevel()),
'FQDN': str(self.Fqdn()),
'isLaptop': str(self.IsLaptop()),
'Manufacturer': str(self.ComputerManufacturer()),
'Model': str(self.ComputerModel()),
'OS': str(self.ComputerOs()),
'release': str(self.Release()),
'Release Path': str(self.ReleasePath()),
'SerialNumber': str(self.ComputerSerial()),
'Support Tier': str(self.SupportTier()),
'tpm_present': str(self.TpmPresent()),
'is_virtual': str(self.IsVirtual()),
}
}
# chooser data
user_data = self._chooser_responses
if user_data:
for key in user_data:
build_data['BUILD'][key] = str(user_data[key])
# timers
t = self._timers.GetAll()
for key in t:
build_data['BUILD']['TIMER_%s' % key] = str(t[key])
with open(to_file, 'w') as handle:
yaml.dump(build_data, handle)
def _StringPinner(self, check_list, match_list, loose=False):
"""Checks a list of strings for acceptable matches.
The check_list of strings should be one or more strings we want to verify,
such as the computer model.
The match_list is a list of strings which we will verify against, such as
a list of pinned computer models.
A direct match occurs when any one entry in check_list matches any one
entry in match_list. If loose is True, the direct match will happen if
any one full string in check_list matches the beginning of any string in
match_list.
Also supports inverse pinning. Inverse pins are strings starting with an
exclamation point (!). An inverse pin returns False if any one match
string matches the inverse string (minus the !).
Inverse pinning results in all non-list elements being treated as matches.
If the set is not directly negated by a matching inverse pin, the outcome
is a successful match. For example:
[!A, !B] returns False for A and False for B, but True for C.
Any check_list with at least one inverse pin is treated strictly as an
inverse set. Direct pins are only considered if no inverse pins are
present. This is to compensate for direct matches being exclusive in
nature. It would not make sense to supply [!A, !B, C], because [C] would
have the same result.
All strings are compared in lowered case.
Args:
check_list: List of known strings.
match_list: List of acceptable strings.
loose: Accept partial matches (start of string only).
Returns:
True for a match between check_list and match_list, else False.
"""
if not check_list or not match_list:
logging.debug('Invalid string comparison sets. [%s, %s]', check_list,
match_list)
return False
inverse_in_set = False
for pin in match_list:
if not pin:
continue
pin = str(pin).lower()
if pin[0] == '!':
for item in check_list:
real_pin = pin[1:]
if ((loose and str(item).lower().startswith(real_pin)) or
(not loose and real_pin == str(item).lower())):
logging.debug('Excluded by inverse pin. [%s]', item)
return False
inverse_in_set = True
if inverse_in_set:
logging.debug('Included by inverse pinning.')
return True
for pin in match_list:
pin = str(pin).lower()
for item in check_list:
if ((loose and str(item).lower().startswith(pin)) or
(not loose and pin == str(item).lower())):
logging.debug('Included by direct pin. [%s]', item)
return True
return False
def SupportedModels(self):
"""Returns the list of known supported models (tier1 and tier2).
Returns:
A dict of two elements, tier1 and tier2, each with a list of models.
"""
supported_models = {}
models = self._ReleaseInfo()['supported_models']
supported_models['tier1'] = [
str(model).lower() for model in models['tier1']
]
supported_models['tier2'] = [
str(model).lower() for model in models['tier2']
]
return supported_models
def SupportTier(self):
"""Determines the support tier for the current device.
Returns:
0 = unknown or totally unsupported platform
1 = tier1, fully supported platform
2 = tier2, best effort/partial support
"""
model = self.ComputerModel()
supported = self.SupportedModels()
if self._StringPinner([model], supported['tier1'], loose=True):
logging.debug('Model %s is fully supported: tier1.', model)
return 1
if self._StringPinner([model], supported['tier2'], loose=True):
logging.debug('Model %s is partially supported: tier2.', model)
return 2
logging.debug('Model %s is not recognized as supported.', model)
return 0
def TimerGet(self, name):
return self._timers.Get(name)
def TimerSet(self, name):
self._timers.Set(name)
def _TpmInfo(self):
if not self._tpm_info:
self._tpm_info = tpm_info.TpmInfo()
return self._tpm_info
def TpmPresent(self):
"""Get the TPM presence from WMI.
Returns:
True if a TPM is present, else False.
"""
return self._TpmInfo().TpmPresent()
def VideoControllers(self):
"""Get any local video (graphics) controllers.
Returns:
A list containing the detected devices.
"""
return self._HWInfo().VideoControllers()
def VideoControllersByName(self):
"""Get all names of detected video controllers.
Returns:
A list containing the names of the detected devices.
"""
names = []
for v in self.VideoControllers():
names.append(v['name'])
return names
def WinpeVersion(self):
"""The production WinPE version according to the distribution source."""
return self._VersionInfo()['winpe-version']
def Branch(self):
"""Determine the current build branch.
Returns:
The build branch as a string.
Raises:
BuildInfoError: Reference to an unknown operating system.
"""
versions = self.KnownBranches()
comp_os = self.ComputerOs()
if not comp_os:
raise BuildInfoError('Unable to determine host OS.')
if comp_os in versions:
return versions[comp_os]
raise BuildInfoError('Unable to find a release that supports %s.', comp_os)

497
lib/buildinfo_test.py Normal file
View File

@@ -0,0 +1,497 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.buildinfo."""
import datetime
from fakefs import fake_filesystem
from glazier.lib import buildinfo
from gwinpy.wmi.hw_info import DeviceId
import mock
import yaml
import unittest
_RELEASE_INFO = """
supported_models:
tier1:
[
Windows Tier 1 Device, # Testing
]
tier2:
[
Windows Tier 2 Device, # Testing
]
"""
_VERSION_INFO = """
winpe-version: 12345
versions:
windows-7-stable: 'stable'
windows-10-stable: 'stable'
windows-10-unstable: 'unstable'
"""
class BuildInfoTest(unittest.TestCase):
def setUp(self):
# fake filesystem
self.filesystem = fake_filesystem.FakeFilesystem()
self.filesystem.CreateDirectory('/dev')
buildinfo.os = fake_filesystem.FakeOsModule(self.filesystem)
buildinfo.open = fake_filesystem.FakeFileOpen(self.filesystem)
# setup
mock_wmi = mock.patch.object(
buildinfo.hw_info.wmi_query, 'WMIQuery', autospec=True)
self.addCleanup(mock_wmi.stop)
mock_wmi.start()
self.buildinfo = buildinfo.BuildInfo()
def testChooserOptions(self):
opt1 = {
'name': 'system_locale',
'type': 'radio_menu',
'prompt': 'System Locale',
'options': []
}
opt2 = {
'name': 'core_ps_shell',
'type': 'toggle',
'prompt': 'Set system shell to PowerShell',
'options': []
}
self.buildinfo.AddChooserOption(opt1)
self.buildinfo.AddChooserOption(opt2)
back = self.buildinfo.GetChooserOptions()
self.assertEqual(back[0]['name'], 'system_locale')
self.assertEqual(back[1]['name'], 'core_ps_shell')
self.assertEqual(len(back), 2)
self.buildinfo.FlushChooserOptions()
back = self.buildinfo.GetChooserOptions()
self.assertEqual(len(back), 0)
def testStoreChooserResponses(self):
"""Store responses from the Chooser UI."""
resp = {'system_locale': 'en-us', 'core_ps_shell': True}
self.buildinfo.StoreChooserResponses(resp)
self.assertEqual(self.buildinfo._chooser_responses['USER_system_locale'],
'en-us')
self.assertEqual(self.buildinfo._chooser_responses['USER_core_ps_shell'],
True)
def testBinaryPath(self):
result = self.buildinfo.BinaryPath()
expected = 'https://glazier-server.example.com/bin/'
self.assertEqual(result, expected)
def testBuildPinMatch(self):
with mock.patch.object(
buildinfo.BuildInfo, 'ComputerModel', autospec=True) as mock_mod:
mock_mod.return_value = 'HP Z620 Workstation'
# Direct include
self.assertTrue(
self.buildinfo.BuildPinMatch(
'computer_model', ['HP Z640 Workstation', 'HP Z620 Workstation']))
# Direct exclude
self.assertFalse(
self.buildinfo.BuildPinMatch(
'computer_model', ['HP Z640 Workstation', 'HP Z840 Workstation']))
# Inverse exclude
self.assertFalse(
self.buildinfo.BuildPinMatch('computer_model',
['!HP Z620 Workstation']))
# Inverse exclude (second)
self.assertFalse(
self.buildinfo.BuildPinMatch('computer_model', [
'!HP Z840 Workstation', '!HP Z620 Workstation'
]))
# Inverse include
self.assertTrue(
self.buildinfo.BuildPinMatch('computer_model',
['!VMWare Virtual Platform']))
# Inverse include (second)
self.assertTrue(
self.buildinfo.BuildPinMatch('computer_model', [
'!HP Z640 Workstation', '!HP Z840 Workstation'
]))
# Substrings
self.assertTrue(
self.buildinfo.BuildPinMatch('computer_model',
['hp Z840', 'hp Z620']))
# Device Ids
with mock.patch.object(
buildinfo.BuildInfo, 'DeviceIds', autospec=True) as did:
did.return_value = ['WW-XX-YY-ZZ', '11-22-33-44']
# Mismatch
self.assertFalse(
self.buildinfo.BuildPinMatch(
'device_id', ['AA-BB-CC-DD', 'EE-FF-GG-HH', '11-22-33-55']))
# Match
self.assertTrue(
self.buildinfo.BuildPinMatch(
'device_id', ['AA-BB-CC-DD', 'WW-XX-YY-ZZ', 'EE-FF-GG-HH']))
# Match
self.assertTrue(
self.buildinfo.BuildPinMatch('device_id',
['AA-BB-CC-DD', 'WW-XX-YY-VV', '11-22']))
# Strict matches
with mock.patch.object(
buildinfo.BuildInfo, 'OsCode', autospec=True) as os:
os.return_value = 'win10'
self.assertTrue(self.buildinfo.BuildPinMatch('os_code', ['win10']))
self.assertTrue(self.buildinfo.BuildPinMatch('os_code', ['WIN10']))
self.assertFalse(self.buildinfo.BuildPinMatch('os_code', ['win7']))
self.assertFalse(self.buildinfo.BuildPinMatch('os_code', ['wi']))
self.assertFalse(self.buildinfo.BuildPinMatch('os_code', ['']))
# Invalid pin
self.assertRaises(buildinfo.BuildInfoError, self.buildinfo.BuildPinMatch,
'no_existo', ['invalid pin value'])
def testBuildUserPinMatch(self):
self.buildinfo.StoreChooserResponses({'puppet': True, 'locale': 'de-de'})
self.assertFalse(self.buildinfo.BuildPinMatch('USER_puppet', [False]))
self.assertTrue(self.buildinfo.BuildPinMatch('USER_puppet', [True]))
self.assertTrue(
self.buildinfo.BuildPinMatch('USER_locale', ['en-us', 'de-de']))
self.assertFalse(
self.buildinfo.BuildPinMatch('USER_locale', ['en-us', 'fr-fr']))
self.assertFalse(self.buildinfo.BuildPinMatch('USER_locale', []))
self.assertFalse(self.buildinfo.BuildPinMatch('USER_missing', ['na']))
def testCache(self):
self.assertEqual(self.buildinfo.Cache().Path(),
buildinfo.constants.SYS_CACHE)
@mock.patch.object(
buildinfo.hw_info.HWInfo, 'ComputerSystemManufacturer', autospec=True)
def testComputerManufacturer(self, mock_man):
mock_man.return_value = 'Google Inc.'
result = self.buildinfo.ComputerManufacturer()
self.assertEqual(result, 'Google Inc.')
mock_man.return_value = None
self.assertRaises(buildinfo.BuildInfoError,
self.buildinfo.ComputerManufacturer)
@mock.patch.object(
buildinfo.hw_info.HWInfo, 'ComputerSystemModel', autospec=True)
def testComputerModel(self, sys_model):
sys_model.return_value = 'HP Z620 Workstation'
result = self.buildinfo.ComputerModel()
self.assertEqual(result, 'HP Z620 Workstation')
sys_model.return_value = '2537CE2'
self.assertEqual(result, 'HP Z620 Workstation') # caching
result = self.buildinfo.ComputerModel()
self.assertEqual(result, '2537CE2')
sys_model.return_value = None
self.assertRaises(buildinfo.BuildInfoError, self.buildinfo.ComputerModel)
def testHostSpecFlags(self):
old_spec = buildinfo.spec.FLAGS.glazier_spec
buildinfo.spec.FLAGS.glazier_spec = 'flag'
buildinfo.spec.FLAGS.glazier_spec_hostname = 'TEST-HOST'
buildinfo.spec.FLAGS.glazier_spec_fqdn = 'TEST-HOST.example.com'
self.assertEqual(self.buildinfo.ComputerName(), 'TEST-HOST')
self.assertEqual(self.buildinfo.Fqdn(), 'TEST-HOST.example.com')
# clean up
buildinfo.spec.FLAGS.glazier_spec = old_spec
def testOsSpecFlags(self):
old_spec = buildinfo.spec.FLAGS.glazier_spec
buildinfo.spec.FLAGS.glazier_spec = 'flag'
buildinfo.spec.FLAGS.glazier_spec_os = 'windows-10-test'
self.assertEqual(self.buildinfo.ComputerOs(), 'windows-10-test')
# clean up
buildinfo.spec.FLAGS.glazier_spec = old_spec
@mock.patch.object(buildinfo.hw_info.HWInfo, 'BiosSerial', autospec=True)
def testComputerSerial(self, bios_serial):
bios_serial.return_value = '5KD1BP1'
result = self.buildinfo.ComputerSerial()
self.assertEqual(result, '5KD1BP1')
@mock.patch.object(buildinfo.hw_info.HWInfo, 'PciDevices', autospec=True)
def testDeviceIds(self, mock_pci):
test_dev = DeviceId(ven='8086', dev='1E10', subsys='21FB17AA', rev='C4')
mock_pci.return_value = [test_dev]
self.assertEqual(['8086-1E10-21FB17AA-C4'], self.buildinfo.DeviceIds())
def testDeviceIdPinning(self):
local_ids = ['11-22-33-44', 'AA-BB-CC-DD', 'AA-BB-CC-DD']
self.assertTrue(
self.buildinfo._StringPinner(
local_ids, ['AA-BB-CC-DD'], loose=True))
self.assertTrue(
self.buildinfo._StringPinner(
local_ids, ['AA-BB-CC'], loose=True))
self.assertTrue(
self.buildinfo._StringPinner(
local_ids, ['AA-BB'], loose=True))
self.assertTrue(self.buildinfo._StringPinner(local_ids, ['AA'], loose=True))
self.assertFalse(
self.buildinfo._StringPinner(
local_ids, ['DD-CC-BB-AA'], loose=True))
self.assertFalse(
self.buildinfo._StringPinner(
local_ids, ['BB-CC'], loose=True))
@mock.patch.object(buildinfo.hw_info, 'HWInfo', autospec=True)
def testHWInfo(self, hw_info):
result = self.buildinfo._HWInfo()
self.assertEqual(result, hw_info.return_value)
self.assertEqual(self.buildinfo._hw_info, hw_info.return_value)
@mock.patch.object(buildinfo.hw_info.HWInfo, 'IsLaptop', autospec=True)
def testIsLaptop(self, mock_lap):
mock_lap.return_value = True
self.assertTrue(self.buildinfo.IsLaptop())
mock_lap.return_value = False
self.assertFalse(self.buildinfo.IsLaptop())
@mock.patch.object(
buildinfo.hw_info.HWInfo, 'IsVirtualMachine', autospec=True)
def testIsVirtual(self, virt):
virt.return_value = False
self.assertFalse(self.buildinfo.IsVirtual())
virt.return_value = True
self.assertTrue(self.buildinfo.IsVirtual())
@mock.patch.object(buildinfo.BuildInfo, '_ReleaseInfo', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'ComputerOs', autospec=True)
def testOsCode(self, comp_os, rel_info):
codes = {
'os_codes': {
'windows-7-stable-x64': {
'code': 'win7'
},
'windows-10-stable': {
'code': 'win10'
},
'win2012r2-x64-se': {
'code': 'win2012r2-x64-se'
}
}
}
rel_info.return_value = codes
comp_os.return_value = 'windows-10-stable'
self.assertEqual(self.buildinfo.OsCode(), 'win10')
comp_os.return_value = 'win2012r2-x64-se'
self.assertEqual(self.buildinfo.OsCode(), 'win2012r2-x64-se')
comp_os.return_value = 'win2000-x64-se'
self.assertRaises(buildinfo.BuildInfoError, self.buildinfo.OsCode)
@mock.patch.object(buildinfo.net_info, 'NetInfo', autospec=True)
def testNetInterfaces(self, netinfo):
netinfo.return_value.Interfaces.return_value = [
mock.Mock(
description='d1', mac_address='11:22:33:44:55'),
mock.Mock(
description='d2', mac_address='AA:BB:CC:DD:EE'),
mock.Mock(
description='d3', mac_address='AA:22:CC:44:EE'),
]
ints = self.buildinfo.NetInterfaces()
self.assertEqual(ints[1].description, 'd2')
netinfo.assert_called_with(poll=True, active_only=True)
ints = self.buildinfo.NetInterfaces(False)
netinfo.assert_called_with(poll=True, active_only=False)
@mock.patch.object(buildinfo.files, 'Read', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'Branch', autospec=True)
def testRelease(self, branch, fread):
branch.return_value = 'unstable'
fread.return_value = {'release_id': '1234'}
self.assertEqual(self.buildinfo.Release(), '1234')
fread.assert_called_with(
'https://glazier-server.example.com/unstable/release-id.yaml')
fread.return_value = {'no_release_id': '1234'}
self.assertEqual(self.buildinfo.Release(), None)
# read error
fread.side_effect = buildinfo.files.Error
self.assertRaises(buildinfo.BuildInfoError, self.buildinfo.Release)
@mock.patch.object(buildinfo.files, 'Read', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'Branch', autospec=True)
def testReleaseInfo(self, branch, fread):
branch.return_value = 'testing'
fread.return_value = {}
self.buildinfo._ReleaseInfo()
fread.assert_called_with(
'https://glazier-server.example.com/testing/release-info.yaml')
# read error
fread.side_effect = buildinfo.files.Error
self.assertRaises(buildinfo.BuildInfoError, self.buildinfo._ReleaseInfo)
@mock.patch.object(buildinfo.files, 'Read', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'ComputerOs', autospec=True)
def testReleasePath(self, comp_os, read):
read.return_value = yaml.safe_load(_VERSION_INFO)
comp_os.return_value = 'windows-7-stable'
expected = 'https://glazier-server.example.com/stable/'
self.assertEqual(self.buildinfo.ReleasePath(), expected)
comp_os.return_value = 'windows-10-unstable'
expected = 'https://glazier-server.example.com/unstable/'
self.assertEqual(self.buildinfo.ReleasePath(), expected)
# no os
comp_os.return_value = None
self.assertRaises(buildinfo.BuildInfoError, self.buildinfo.ReleasePath)
# invalid os
comp_os.return_value = 'invalid-os-string'
self.assertRaises(buildinfo.BuildInfoError, self.buildinfo.ReleasePath)
def testActiveConfigPath(self):
self.buildinfo.ActiveConfigPath(append='/foo')
self.buildinfo.ActiveConfigPath(append='/bar')
self.assertEqual(self.buildinfo.ActiveConfigPath(), ['/foo', '/bar'])
self.assertEqual(self.buildinfo.ActiveConfigPath(pop=True), ['/foo'])
self.assertEqual(self.buildinfo.ActiveConfigPath(pop=True), [])
self.assertEqual(self.buildinfo.ActiveConfigPath(pop=True), [])
self.buildinfo.ActiveConfigPath(set_to=['/foo', 'bar', 'baz'])
self.assertEqual(self.buildinfo.ActiveConfigPath(), ['/foo', 'bar', 'baz'])
def testStringPinner(self):
self.assertFalse(self.buildinfo._StringPinner(['A', 'B'], []))
self.assertFalse(self.buildinfo._StringPinner(['A', 'B'], None))
self.assertFalse(self.buildinfo._StringPinner([], ['A', 'B']))
self.assertFalse(self.buildinfo._StringPinner(None, ['A', 'B']))
self.assertTrue(self.buildinfo._StringPinner(['A'], ['A', 'B']))
self.assertTrue(self.buildinfo._StringPinner(['B'], ['A', 'B']))
self.assertTrue(self.buildinfo._StringPinner(['A'], ['!C', '!D']))
self.assertFalse(self.buildinfo._StringPinner(['D'], ['!C', '!D']))
self.assertFalse(self.buildinfo._StringPinner([True], [False]))
self.assertFalse(self.buildinfo._StringPinner([False], [True]))
self.assertTrue(self.buildinfo._StringPinner([True], [True]))
self.assertTrue(self.buildinfo._StringPinner([False], [False]))
@mock.patch.object(buildinfo.files, 'Read', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'ReleasePath', autospec=True)
def testSupportedModels(self, unused_rel_path, fread):
fread.return_value = yaml.safe_load(_RELEASE_INFO)
results = self.buildinfo.SupportedModels()
self.assertIn('tier1', results)
self.assertIn('tier2', results)
for model in results['tier1'] + results['tier2']:
self.assertEqual(type(model), str)
self.assertIn('windows tier 1 device', results['tier1'])
self.assertIn('windows tier 2 device', results['tier2'])
@mock.patch.object(buildinfo.BuildInfo, 'ComputerModel', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'SupportedModels', autospec=True)
def testSupportTier(self, mock_supp, mock_comp):
# Tier1
mock_comp.return_value = 'VMWare Virtual Platform'
mock_supp.return_value = {
'tier1': ['vmware virtual platform', 'hp z620 workstation'],
'tier2': ['precision workstation t3400', '20BT'],
}
self.assertEqual(self.buildinfo.SupportTier(), 1)
# Tier 2
mock_comp.return_value = 'Precision WorkStation T3400'
self.assertEqual(self.buildinfo.SupportTier(), 2)
# Partial Match
mock_comp.return_value = '20BTS0A400'
self.assertEqual(self.buildinfo.SupportTier(), 2)
# Unsupported
mock_comp.return_value = 'Best Buy Special of the Day'
self.assertEqual(self.buildinfo.SupportTier(), 0)
@mock.patch.object(buildinfo.tpm_info, 'TpmInfo', autospec=True)
def testTpmInfo(self, tpm_info):
result = self.buildinfo._TpmInfo()
self.assertEqual(result, tpm_info.return_value)
self.assertEqual(self.buildinfo._tpm_info, tpm_info.return_value)
@mock.patch.object(buildinfo.tpm_info.TpmInfo, 'TpmPresent', autospec=True)
def testTpmPresent(self, tpm_present):
tpm_present.return_value = True
self.assertTrue(self.buildinfo.TpmPresent())
tpm_present.return_value = False
self.assertTrue(self.buildinfo.TpmPresent()) # caching
self.assertFalse(self.buildinfo.TpmPresent())
@mock.patch.object(buildinfo.files, 'Read', autospec=True)
def testWinpeVersion(self, fread):
fread.return_value = yaml.safe_load(_VERSION_INFO)
self.assertEqual(type(self.buildinfo.WinpeVersion()), int)
@mock.patch.object(buildinfo.BuildInfo, 'ComputerModel', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'IsVirtual', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'TpmPresent', autospec=True)
@mock.patch.object(buildinfo.logging, 'info', autospec=True)
def testEncryptionLevel(self, info, tpm, virtual, model):
model.return_value = 'HP Z440 Workstation'
tpm.return_value = False
virtual.return_value = True
# virtual machine
self.assertEqual(self.buildinfo.EncryptionLevel(), 'none')
info.assert_called_with(mock.REGEXP('^Virtual machine type .*'), mock.ANY)
virtual.return_value = False
# tpm
tpm.return_value = True
self.assertEqual(self.buildinfo.EncryptionLevel(), 'tpm')
info.assert_called_with(mock.REGEXP('^TPM detected .*'))
# default
self.assertEqual(self.buildinfo.EncryptionLevel(), 'tpm')
def testSerialize(self):
mock_b = mock.Mock(spec_set=self.buildinfo)
mock_b._chooser_responses = {
'USER_choice_one': 'value1',
'USER_choice_two': 'value2'
}
mock_b._timers.GetAll.return_value = {
'timer_1': datetime.datetime.utcnow()
}
mock_b.Serialize = buildinfo.BuildInfo.Serialize.__get__(mock_b)
mock_b.Serialize('/build_info.yaml')
parsed = yaml.safe_load(buildinfo.open('/build_info.yaml'))
self.assertIn('branch', parsed['BUILD'])
self.assertIn('Model', parsed['BUILD'])
self.assertIn('SerialNumber', parsed['BUILD'])
self.assertIn('USER_choice_two', parsed['BUILD'])
self.assertIn('TIMER_timer_1', parsed['BUILD'])
self.assertEqual(parsed['BUILD']['USER_choice_two'], 'value2')
@mock.patch.object(buildinfo.timers.datetime, 'datetime', autospec=True)
def testTimers(self, dt):
now = datetime.datetime.utcnow()
dt.utcnow.return_value = now
self.buildinfo.TimerSet('test_timer_1')
self.assertEqual(self.buildinfo.TimerGet('test_timer_2'), None)
self.assertEqual(self.buildinfo.TimerGet('test_timer_1'), now)
@mock.patch.object(
buildinfo.hw_info.HWInfo, 'VideoControllers', autospec=True)
def testVideoControllers(self, controllers):
controllers.return_value = [{
'name': 'NVIDIA Quadro 600'
}, {
'name': 'Intel(R) HD Graphics 4000'
}]
result = self.buildinfo.VideoControllers()
self.assertEqual(result[0]['name'], 'NVIDIA Quadro 600')
self.assertTrue(
self.buildinfo.BuildPinMatch(
'graphics', ['Intel(R) HD Graphics 3000', 'NVIDIA Quadro 600']))
self.assertFalse(
self.buildinfo.BuildPinMatch(
'graphics', ['Intel(R) HD Graphics 3000', 'NVIDIA Quadro 500']))
if __name__ == '__main__':
unittest.main()

94
lib/cache.py Normal file
View File

@@ -0,0 +1,94 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Manages the on-disk build cache."""
import os
import re
from glazier.lib import constants
from glazier.lib import download
DNLD_RE = re.compile(r'([@|#][\S]+)')
class CacheError(Exception):
pass
class Cache(object):
"""Handles interation with the on-disk build cache."""
def __init__(self):
self._downloader = download.Download(show_progress=False)
def _DestinationPath(self, url):
"""Determines the local path for a file being downloaded.
Args:
url: A web address to a file as a string
Returns:
The local disk path as a string.
"""
file_name = url.split('/').pop()
destination = os.path.join(self.Path(), file_name)
return destination
def _FindDownload(self, line):
"""Searches a command line for any download strings.
Args:
line: the command line to search
Returns:
the url which requires downloading or none
"""
result = DNLD_RE.search(line)
if result:
return result.group(1).rstrip('"\'')
return None
def CacheFromLine(self, line, build_info):
"""Downloads any files in the command line and replaces with the local path.
Args:
line: the command line to process as a string
build_info: the current build information
Returns:
the final command line as a string; None on error
Raises:
CacheError: unable to download a file to the local cache
"""
match = self._FindDownload(line)
while match:
dl = download.Transform(match, build_info)
destination = self._DestinationPath(dl)
try:
self._downloader.DownloadFile(dl, destination)
except download.DownloadError as e:
self._downloader.PrintDebugInfo()
raise CacheError('Unable to download required file %s: %s' % (dl, e))
line = line.replace(match, destination)
match = self._FindDownload(line)
return line
def Path(self):
"""Returns the path to the local build cache.
Returns:
the path to the local build cache
"""
return constants.SYS_CACHE

86
lib/cache_test.py Normal file
View File

@@ -0,0 +1,86 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.cache."""
import os
from fakefs import fake_filesystem
from glazier.lib import cache
import mock
import unittest
class CacheTest(unittest.TestCase):
def setUp(self):
self.cache = cache.Cache()
fakefs = fake_filesystem.FakeFilesystem()
fakefs.CreateDirectory(r'C:\Directory')
os_module = fake_filesystem.FakeOsModule(fakefs)
self.mock_open = fake_filesystem.FakeFileOpen(fakefs)
cache.os = os_module
cache.open = self.mock_open
def MockTransform(self, string, unused_info):
if '#' in string:
string = string.replace('#', 'https://test.example.com/release/')
if '@' in string:
string = string.replace('@', 'https://test.example.com/bin/')
return string
@mock.patch.object(cache.download, 'Transform', autospec=True)
@mock.patch.object(cache.download.Download, 'DownloadFile', autospec=True)
def testCacheFromLine(self, download, transform):
remote1 = r'folder/other/installer.msi'
remote2 = r'config_file.conf'
local1 = os.path.join(self.cache.Path(), 'installer.msi')
local2 = os.path.join(self.cache.Path(), 'config_file.conf')
line_in = 'msiexec /i @%s /qa /l*v CONF=#%s' % (remote1, remote2)
line_out = 'msiexec /i %s /qa /l*v CONF=%s' % (local1, local2)
download.return_value = True
transform.side_effect = self.MockTransform
result = self.cache.CacheFromLine(line_in, None)
self.assertEqual(result, line_out)
call1 = mock.call(self.cache._downloader,
'https://test.example.com/bin/%s' % remote1, local1)
call2 = mock.call(self.cache._downloader,
'https://test.example.com/release/%s' % remote2, local2)
download.assert_has_calls([call1, call2])
# download exception
transfer_err = cache.download.DownloadError('Error message.')
download.side_effect = transfer_err
self.assertRaises(cache.CacheError, self.cache.CacheFromLine,
'@%s' % remote2, None)
def testDestinationPath(self):
path = self.cache._DestinationPath('http://some.web.address/folder/other/'
'an_installer.msi')
self.assertEqual(path, os.path.join(self.cache.Path(), 'an_installer.msi'))
def testFindDownload(self):
line_test = self.cache._FindDownload('powershell -file '
r'C:\run_some_file.ps1')
self.assertEqual(line_test, None)
line_test = self.cache._FindDownload('msiexec /i @installer.msi /qa')
self.assertEqual(line_test, '@installer.msi')
line_test = self.cache._FindDownload(r'C:\install_some_program.exe '
'/i ARGS=FOO')
self.assertEqual(line_test, None)
line_test = self.cache._FindDownload(
'some_executable.exe /conf=#remote.conf /flag1 /flag1')
self.assertEqual(line_test, '#remote.conf')
if __name__ == '__main__':
unittest.main()

13
lib/config/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

62
lib/config/base.py Normal file
View File

@@ -0,0 +1,62 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Configuration handling core functionality.
This class contains features common to both the Config Builder and Config Runner
classes. It is meant to be inherited rather than run directly.
"""
from glazier.lib import actions
class ConfigError(Exception):
pass
class ConfigBase(object):
"""Core functionality for the configuration handling module."""
def __init__(self, build_info):
self._build_info = build_info
def _GetAction(self, action, params):
try:
act_obj = getattr(actions, str(action))
return act_obj(args=params, build_info=self._build_info)
except AttributeError:
raise ConfigError('Unknown imaging action: %s' % str(action))
def _IsRealtimeAction(self, action, params):
"""Determine whether $action should happen in realtime."""
if action not in dir(actions):
return False
a = self._GetAction(action, params)
return a.IsRealtime()
def _ProcessAction(self, action, params):
"""Attempt to process a dynamic action element.
Args:
action: The name of the action.
params: The params being passed in with the action.
Raises:
ConfigError: The action is either undefined, or failed to execute.
"""
try:
a = self._GetAction(action, params)
a.Run()
except actions.ActionError as e:
raise ConfigError(str(e))

45
lib/config/base_test.py Normal file
View File

@@ -0,0 +1,45 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.config.base."""
from glazier.lib import buildinfo
from glazier.lib.config import base
import mock
import unittest
class BaseTest(unittest.TestCase):
def setUp(self):
self.buildinfo = buildinfo.BuildInfo()
self.cb = base.ConfigBase(self.buildinfo)
@mock.patch.object(base.actions, 'SetTimer', autospec=True)
def testProcessActions(self, set_timer):
# valid command
self.cb._ProcessAction('SetTimer', ['TestTimer'])
set_timer.assert_called_with(build_info=self.buildinfo, args=['TestTimer'])
self.assertTrue(set_timer.return_value.Run.called)
# invalid command
self.assertRaises(base.ConfigError, self.cb._ProcessAction, 'BadSetTimer',
['Timer1'])
# action error
set_timer.side_effect = base.actions.ActionError
self.assertRaises(base.ConfigError, self.cb._ProcessAction, 'SetTimer',
['Timer1'])
if __name__ == '__main__':
unittest.main()

165
lib/config/builder.py Normal file
View File

@@ -0,0 +1,165 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Manages the initial processing of build.yaml files.
## Overview
The Config Builder is responsible for compiling a custom installation task list
for the local host. It traces through all yaml configuration files linked to
the root config, and stores any commands that are determined applicable by
pinning (or lack thereof).
The end result of running ConfigBuilder is a single ordered list of tasks to
be performed when installing the host.
## Use
Start is the main entry point for the class. It expects a path to a
configuration directory and a filename (the base build.yaml).
## Include Logic
Yaml files can refer to one another via the include directive. Each sub-yaml
is treated identically to the base file, following the exact same logic. The
include directive calls Start on each included file, completing that file
top-to-bottom before returning to the caller.
BuildInfo tracks our position in the directory tree. Additional levels
(includes in sub-directories) are pushed onto the ActiveConfigPath while we
process the include. At the end of processing the stack is popped, returning us
to our previous level. This allows us to reference other files relative to the
location of the active config.
"""
import copy
from glazier.lib import actions
from glazier.lib import buildinfo
from glazier.lib import download
from glazier.lib.config import base
from glazier.lib.config import files
_ALLOW_IN_TEMPLATE = [
'include',
'template',
'execute',
'policy',
] + dir(actions)
_ALLOW_IN_CONTROL = _ALLOW_IN_TEMPLATE + ['pin']
class ConfigBuilderError(base.ConfigError):
pass
class ConfigBuilder(base.ConfigBase):
"""Builds the complete task list for the installation."""
def Start(self, out_file, in_path, in_file='build.yaml'):
"""Start parsing configuration files.
Args:
out_file: The location to store the compiled config data.
in_path: The path to the root configuration file.
in_file: The root configuration file name.
"""
self._task_list = []
self._Start(in_path, in_file)
try:
files.Dump(out_file, self._task_list, mode='a')
except files.Error as e:
raise ConfigBuilderError(e)
def _Start(self, conf_path, conf_file):
"""Pull and process a config file.
Args:
conf_path: The path to the config below root.
conf_file: A named config file, normally build.yaml.
"""
self._build_info.ActiveConfigPath(append=conf_path.rstrip('/'))
try:
path = download.PathCompile(self._build_info, file_name=conf_file)
yaml_config = files.Read(path)
except (files.Error, buildinfo.BuildInfoError) as e:
raise ConfigBuilderError(e)
controls = yaml_config['controls']
for control in controls:
if 'pin' not in control or self._MatchPin(control['pin']):
self._StoreControls(control, yaml_config.get('templates'))
self._build_info.ActiveConfigPath(pop=True)
def _MatchPin(self, pins):
"""Check all pin entries for a mismatch.
Pins can mismatch either by the matching setting being omitted or by
matching an exclusion (!).
Example:
pins: ['os', ['win7', 'win8']]
* Will match os = win7 or os = win8.
* Will fail to match os = '2012r2'.
* Will match model = 'vmware' (because model is not pinned).
Example 2:
pins: ['os', ['!win7']]
* Will match os = win8 or os = 2012r2.
* Will fail to match os = 'win7'.
* Will match model = 'vmware' (because model is not pinned).
Args:
pins: a list of all applicable pin names and acceptable values
Returns:
True if this host passes all pin checks. False if the host fails a match.
"""
for pin in pins:
try:
if not self._build_info.BuildPinMatch(pin, pins[pin]):
return False
except buildinfo.BuildInfoError as e:
raise ConfigBuilderError('Error gathering system information. %s' % e)
return True
def _StoreControls(self, control, templates):
"""Process all of the possible sub-sections of a main control section.
Args:
control: The data from this control subsection.
templates: Any templates declared in the current config.
Raises:
ConfigBuilderError: Attempt to process an unknown command element.
"""
for element in control:
if element == 'pin':
continue
elif element == 'template':
for template in control['template']:
self._StoreControls(templates[template], templates)
elif element == 'include':
for sub_inc in control['include']:
self._Start(conf_path=sub_inc[0], conf_file=sub_inc[1])
elif element in _ALLOW_IN_CONTROL:
if self._IsRealtimeAction(element, control[element]):
self._ProcessAction(element, control[element])
else:
self._task_list.append({
'path': copy.deepcopy(self._build_info.ActiveConfigPath()),
'data': {element: control[element]}
})
else:
raise ConfigBuilderError('Unknown imaging action: %s' % str(element))

View File

@@ -0,0 +1,90 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.config.builder."""
from fakefs import fake_filesystem
from glazier.lib import buildinfo
from glazier.lib.config import builder
import mock
import unittest
class ConfigBuilderTest(unittest.TestCase):
def setUp(self):
self.buildinfo = buildinfo.BuildInfo()
# filesystem
self.filesystem = fake_filesystem.FakeFilesystem()
self.cb = builder.ConfigBuilder(self.buildinfo)
self.cb._task_list = []
@mock.patch.object(buildinfo.BuildInfo, 'BuildPinMatch', autospec=True)
def testMatchPin(self, bpm):
# All direct matching.
bpm.side_effect = iter([True, True])
pins = {
'computer_model': [
'HP Z640 Workstation',
'HP Z620 Workstation',
],
'os_code': ['win7']
}
self.assertTrue(self.cb._MatchPin(pins))
# Inverse match + direct match.
bpm.side_effect = iter([False, True])
pins = {
'computer_model': [
'HP Z640 Workstation',
'!HP Z620 Workstation',
],
'os_code': ['win7']
}
self.assertFalse(self.cb._MatchPin(pins))
# Inverse miss.
bpm.side_effect = iter([True, False])
pins = {
'computer_model': ['!VMWare Virtual Platform'],
'os_code': ['win8']
}
self.assertFalse(self.cb._MatchPin(pins))
# Empty set.
pins = {}
self.assertTrue(self.cb._MatchPin(pins))
# Inverse miss + direct mismatch.
bpm.side_effect = iter([False, False])
pins = {
'computer_model': ['VMWare Virtual Platform'],
'os_code': ['win8']
}
self.assertFalse(self.cb._MatchPin(pins))
# Error
bpm.side_effect = buildinfo.BuildInfoError
self.assertRaises(builder.ConfigBuilderError, self.cb._MatchPin, pins)
@mock.patch.object(builder.ConfigBuilder, '_ProcessAction', autospec=True)
def testRealtime(self, process):
config = {'ShowChooser': ['Chooser Stuff']}
self.cb._StoreControls(config, {})
process.assert_called_with(mock.ANY, 'ShowChooser', ['Chooser Stuff'])
process.reset_mock()
config = {'CopyFile': [r'C:\input.txt', r'C:\output.txt']}
self.cb._StoreControls(config, {})
self.assertFalse(process.called)
self.assertEqual(self.cb._task_list[0]['data'], config)
if __name__ == '__main__':
unittest.main()

82
lib/config/files.py Normal file
View File

@@ -0,0 +1,82 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Functions for interacting with yaml configuration files."""
import re
from glazier.lib import download
from glazier.lib import file_util
import yaml
class Error(Exception):
pass
def Dump(path, data, mode='w'):
"""Write a config file containing some data.
Args:
path: The filesystem path to the destination file.
data: Data to be written to the file as yaml.
mode: Mode to use for writing the file (default: w)
"""
file_util.CreateDirectories(path)
try:
with open(path, mode) as handle:
handle.write(yaml.dump(data))
except IOError as e:
raise Error('Could not save data to yaml file %s: %s' % (path, str(e)))
def Read(path):
"""Read a config file at path and return any data it contains.
Will attempt to download files from remote repositories prior to reading.
Args:
path: The path (either local or remote) to read from.
Returns:
The parsed YAML content from the file.
Raises:
Error: Failure retrieving a remote file or parsing file content.
"""
if re.match('^http(s)?://', path):
downloader = download.Download()
try:
path = downloader.DownloadFileTemp(path)
except download.DownloadError as e:
raise Error('Could not download yaml file %s: %s' % (path, str(e)))
return _YamlReader(path)
def _YamlReader(path):
"""Read a configuration file and return the contents.
Can be overloaded to read configs from different sources.
Args:
path: The config file name (eg build.yaml).
Returns:
The parsed content of the yaml file.
"""
try:
with open(path, 'r') as yaml_file:
yaml_config = yaml.safe_load(yaml_file)
except IOError as e:
raise Error('Could not read yaml file %s: %s' % (path, str(e)))
return yaml_config

66
lib/config/files_test.py Normal file
View File

@@ -0,0 +1,66 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.config.files."""
from fakefs import fake_filesystem
from glazier.lib.config import files
import mock
import unittest
class FilesTest(unittest.TestCase):
def setUp(self):
self.filesystem = fake_filesystem.FakeFilesystem()
files.open = fake_filesystem.FakeFileOpen(self.filesystem)
files.file_util.os = fake_filesystem.FakeOsModule(self.filesystem)
def testDump(self):
op_list = ['op1', ['op2a', 'op2b'], 'op3', {'op4a': 'op4b'}]
files.Dump('/tmp/foo/dump.txt', op_list)
result = files._YamlReader('/tmp/foo/dump.txt')
self.assertEqual(result[1], ['op2a', 'op2b'])
self.assertEqual(result[3], {'op4a': 'op4b'})
self.assertRaises(files.Error, files.Dump, '/tmp', [])
@mock.patch.object(files.download.Download, 'DownloadFileTemp', autospec=True)
def testRead(self, download):
self.filesystem.CreateFile('/tmp/downloaded1.yaml', contents='data: set1')
self.filesystem.CreateFile('/tmp/downloaded2.yaml', contents='data: set2')
download.return_value = '/tmp/downloaded1.yaml'
result = files.Read(
'https://glazier-server.example.com/unstable/dir/test-build.yaml')
download.assert_called_with(
mock.ANY,
'https://glazier-server.example.com/unstable/dir/test-build.yaml')
self.assertEqual(result['data'], 'set1')
# download error
download.side_effect = files.download.DownloadError
self.assertRaises(
files.Error, files.Read,
'https://glazier-server.example.com/unstable/dir/test-build.yaml')
# local
result = files.Read('/tmp/downloaded2.yaml')
self.assertEqual(result['data'], 'set2')
def testYamlReader(self):
self.filesystem.CreateFile(
'/foo/bar/baz.txt', contents='- item4\n- item5\n- item6')
result = files._YamlReader('/foo/bar/baz.txt')
self.assertEqual(result[1], 'item5')
if __name__ == '__main__':
unittest.main()

97
lib/config/runner.py Normal file
View File

@@ -0,0 +1,97 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Manages the execution of the local host task list."""
import sys
from glazier.lib import policies
from glazier.lib import power
from glazier.lib.config import base
from glazier.lib.config import files
class ConfigRunnerError(base.ConfigError):
pass
class ConfigRunner(base.ConfigBase):
"""Executes all steps from the installation task list."""
def Start(self, task_list):
self._task_list_path = task_list
try:
data = files.Read(self._task_list_path)
except files.Error as e:
raise ConfigRunnerError(e)
self._ProcessTasks(data)
def _PopTask(self, tasks):
"""Remove the first event from the task list and save new list to disk."""
tasks.pop(0)
try:
files.Dump(self._task_list_path, tasks, mode='w')
except files.Error as e:
raise ConfigRunnerError(e)
def _ProcessTasks(self, tasks):
"""Process the pending tasks list.
Args:
tasks: The list of pending tasks.
"""
while tasks:
entry = tasks[0]['data']
self._build_info.ActiveConfigPath(set_to=tasks[0]['path'])
for element in entry:
if element == 'policy':
for line in entry['policy']:
self._Policy(line)
else:
try:
self._ProcessAction(element, entry[element])
except base.ConfigError as e:
raise ConfigRunnerError(e)
except base.actions.RestartEvent as e:
if e.task_list_path:
self._task_list_path = e.task_list_path
if not e.retry_on_restart:
self._PopTask(tasks)
power.Restart(e.timeout, e.message)
sys.exit(0)
except base.actions.ShutdownEvent as e:
if e.task_list_path:
self._task_list_path = e.task_list_path
if not e.retry_on_restart:
self._PopTask(tasks)
power.Shutdown(e.timeout, e.message)
sys.exit(0)
self._PopTask(tasks)
def _Policy(self, line):
"""Execute an imaging policy check.
Args:
line: The name of a supported imaging policy.
Raises:
ConfigRunnerError: An imaging policy has raised an exception.
"""
try:
check = getattr(policies, str(line))
policy = check(build_info=self._build_info)
policy.Verify()
except AttributeError:
raise ConfigRunnerError('Unknown imaging policy: %s' % str(line))
except policies.ImagingPolicyException as e:
raise ConfigRunnerError(str(e))

128
lib/config/runner_test.py Normal file
View File

@@ -0,0 +1,128 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.config.runner."""
from fakefs import fake_filesystem
from fakefs import fake_filesystem_shutil
from glazier.lib import buildinfo
from glazier.lib.config import runner
import mock
import unittest
class ConfigRunnerTest(unittest.TestCase):
def setUp(self):
self.buildinfo = buildinfo.BuildInfo()
# filesystem
self.filesystem = fake_filesystem.FakeFilesystem()
runner.os = fake_filesystem.FakeOsModule(self.filesystem)
runner.open = fake_filesystem.FakeFileOpen(self.filesystem)
runner.shutil = fake_filesystem_shutil.FakeShutilModule(self.filesystem)
self.cr = runner.ConfigRunner(self.buildinfo)
self.cr._task_list_path = '/tmp/task_list.yaml'
@mock.patch.object(runner.base.actions, 'pull', autospec=True)
@mock.patch.object(runner.files, 'Dump', autospec=True)
def testIteration(self, dump, unused_get):
conf = [{'data': {'pull': 'val1'},
'path': ['path1']}, {'data': {'pull': 'val2'},
'path': ['path2']}, {'data': {'pull': 'val3'},
'path': ['path3']}]
self.cr._ProcessTasks(conf)
dump.assert_has_calls([
mock.call(
self.cr._task_list_path, conf[1:], mode='w'), mock.call(
self.cr._task_list_path, conf[2:], mode='w'), mock.call(
self.cr._task_list_path, [], mode='w')
])
@mock.patch.object(runner.files, 'Dump', autospec=True)
def testPopTask(self, dump):
self.cr._PopTask([1, 2, 3])
dump.assert_called_with('/tmp/task_list.yaml', [2, 3], mode='w')
dump.side_effect = runner.files.Error
with self.assertRaises(runner.ConfigRunnerError):
self.cr._PopTask([1, 2])
@mock.patch.object(runner.power, 'Restart', autospec=True)
@mock.patch.object(runner.ConfigRunner, '_ProcessAction', autospec=True)
@mock.patch.object(runner.ConfigRunner, '_PopTask', autospec=True)
def testRestartEvents(self, pop, action, restart):
conf = [{'data': {'Shutdown': ['25', 'Reason']}, 'path': ['path1']}]
event = runner.base.actions.RestartEvent('Some reason', timeout=25)
action.side_effect = event
self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
restart.assert_called_with(25, 'Some reason')
self.assertTrue(pop.called)
pop.reset_mock()
# with retry
event = runner.base.actions.RestartEvent(
'Some other reason', timeout=10, retry_on_restart=True)
action.side_effect = event
self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
restart.assert_called_with(10, 'Some other reason')
self.assertFalse(pop.called)
@mock.patch.object(runner.power, 'Shutdown', autospec=True)
@mock.patch.object(runner.ConfigRunner, '_ProcessAction', autospec=True)
@mock.patch.object(runner.ConfigRunner, '_PopTask', autospec=True)
def testShutdownEvents(self, pop, action, shutdown):
conf = [{'data': {'Restart': ['25', 'Reason']}, 'path': ['path1']}]
event = runner.base.actions.ShutdownEvent('Some reason', timeout=25)
action.side_effect = event
self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
shutdown.assert_called_with(25, 'Some reason')
self.assertTrue(pop.called)
pop.reset_mock()
# with retry
event = runner.base.actions.ShutdownEvent(
'Some other reason', timeout=10, retry_on_restart=True)
action.side_effect = event
self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
shutdown.assert_called_with(10, 'Some other reason')
self.assertFalse(pop.called)
@mock.patch.object(runner.base.actions, 'SetTimer', autospec=True)
@mock.patch.object(runner.files, 'Read', autospec=True)
@mock.patch.object(runner.files, 'Dump', autospec=True)
def testProcessActions(self, dump, reader, set_timer):
reader.return_value = [{'data': {'SetTimer': ['TestTimer']},
'path': ['/autobuild']}]
# missing file
reader.side_effect = runner.files.Error
self.assertRaises(runner.ConfigRunnerError, self.cr.Start,
'/tmp/path/missing.yaml')
reader.side_effect = None
# valid command
self.cr.Start('/tmp/path/tasks.yaml')
reader.assert_called_with('/tmp/path/tasks.yaml')
set_timer.assert_called_with(build_info=self.buildinfo, args=['TestTimer'])
self.assertTrue(set_timer.return_value.Run.called)
self.assertTrue(dump.called)
# invalid command
self.assertRaises(runner.ConfigRunnerError, self.cr._ProcessTasks,
[{'data': {'BadSetTimer': ['Timer1']},
'path': ['/autobuild']}])
# action error
set_timer.side_effect = runner.base.actions.ActionError
self.assertRaises(runner.ConfigRunnerError, self.cr._ProcessTasks,
[{'data': {'SetTimer': ['Timer1']},
'path': ['/autobuild']}])
if __name__ == '__main__':
unittest.main()

61
lib/constants.py Normal file
View File

@@ -0,0 +1,61 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants and Flags used by the Glazier imaging code."""
import gflags as flags
BUILD_LOG_FILE = 'glazier.log'
REG_ROOT = r'SOFTWARE\Glazier'
# Network
DOMAIN = 'domain.example.com'
DOMAIN_DN = 'DC=domain,DC=example,DC=com'
USER_AGENT = 'Glazier Installer 1.0'
# System
SYS_ROOT = 'C:'
SYS_CACHE = '%s\\glazier_cache' % SYS_ROOT
SYS_LOGS_PATH = '%s\\Windows\\Logs\\Glazier' % SYS_ROOT
SYS_BUILD_LOG = '%s\\%s' % (SYS_LOGS_PATH, BUILD_LOG_FILE)
SYS_SYSTEM32 = '%s\\Windows\\System32' % SYS_ROOT
SYS_TASK_LIST = '%s\\task_list.yaml' % SYS_CACHE
SYS_POWERSHELL = '%s\\WindowsPowerShell\\v1.0\\powershell.exe' % SYS_SYSTEM32
# WinPE
WINPE_ROOT = 'X:'
WINPE_CACHE = WINPE_ROOT
WINPE_LOGS_PATH = WINPE_ROOT
WINPE_BUILD_LOG = '%s\\%s' % (WINPE_LOGS_PATH, BUILD_LOG_FILE)
WINPE_SYSTEM32 = '%s\\Windows\\System32' % WINPE_ROOT
WINPE_TASK_LIST = '%s\\task_list.yaml' % WINPE_ROOT
WINPE_DISM = '%s\\dism.exe' % WINPE_SYSTEM32
WINPE_POWERSHELL = ('%s\\WindowsPowerShell\\v1.0\\powershell.exe' %
WINPE_SYSTEM32)
## Flags
FLAGS = flags.FLAGS
flags.DEFINE_string('binary_root_path', '/bin', 'Path to the binary storage.')
flags.DEFINE_string('config_root_path', '/autobuild',
'Path to the root of the configuration directory.')
flags.DEFINE_string('config_server', 'https://glazier-server.example.com',
'Root URL for all build data.')
flags.DEFINE_enum('environment', 'Host', ['Host', 'WinPE'],
'The running host environment.')
flags.DEFINE_string('ntp_server', 'time.google.com',
'Server to use for synchronizing the local system time.')

129
lib/domain_join.py Normal file
View File

@@ -0,0 +1,129 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This joins a Windows machine to an Active Directory domain.
Methods:
* auto: Auto join the domain with no user interaction.
* interactive: Prompt the user for domain join credentials.
"""
import logging
import socket
import time
from glazier.lib import buildinfo
from glazier.lib import constants
from glazier.lib import interact
from glazier.lib import powershell
AUTH_OPTS = [
'auto',
'interactive',
]
class DomainJoinError(Exception):
pass
class DomainJoinCredentials(object):
def __init__(self):
self._username = None
self._password = None
def GetUsername(self):
"""Override to provide automatic join credentials."""
return self._username
def GetPassword(self):
"""Override to provide automatic join credentials."""
return self._password
class DomainJoin(object):
"""Defines several functions used to join a machine to the domain."""
def __init__(self, method, domain_name, ou=None):
self._build_info = buildinfo.BuildInfo()
self._domain_name = domain_name
self._domain_ou = ou
self._method = method
self._password = None
self._username = None
def _AutomaticJoin(self):
"""Join the domain with automated credentials."""
creds = DomainJoinCredentials()
self._username = creds.GetUsername()
self._password = creds.GetPassword()
logging.info('Starting automated domain join. Hostname: %s',
socket.gethostname())
while True:
ps = powershell.PowerShell()
try:
logging.debug('Attempting to join the domain %s.', self._domain_name)
ps.RunLocal(
r'%s\join-domain.ps1' % constants.SYS_CACHE,
args=[self._username, self._password, self._domain_name])
except powershell.PowerShellError as e:
logging.error(
'Domain join failed. Sleeping 5 minutes then trying again. (%s)', e)
time.sleep(300)
continue
logging.info('Joined the machine to the domain.')
break
def _SetUsername(self):
self._username = interact.GetUsername()
def _InteractiveJoin(self):
"""Join the domain with user-interactive dialog."""
while True:
self._SetUsername()
ps = powershell.PowerShell()
cmd = [
'Add-Computer', '-DomainName', self._domain_name, '-Credential',
self._username, '-PassThru'
]
if self._domain_ou:
cmd += ['-OUPath', self._domain_ou]
try:
logging.debug('Attempting to join the domain %s.', self._domain_name)
ps.RunCommand(cmd)
except powershell.PowerShellError as e:
logging.error(
'Domain join failed. Sleeping 5 minutes then trying again. (%s)', e)
continue
logging.info('Joined the machine to the domain.')
break
def JoinDomain(self):
"""Perform the domain join operation."""
logging.debug('Beginning domain join process.')
if self._method.startswith('auto'):
self._AutomaticJoin()
else:
self._InteractiveJoin()
logging.info('Domain join completed.')

367
lib/download.py Normal file
View File

@@ -0,0 +1,367 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Download files over HTTPS.
> Resource Requirements
* resources/ca_certs.crt
A certificate file containing permitted root certs for SSL validation.
"""
import hashlib
import logging
import os
import re
import socket
import subprocess
import sys
import tempfile
import time
import urllib2
CHUNK_BYTE_SIZE = 65536
def Transform(string, build_info):
"""Transforms abbreviated file names to absolute file paths.
Short name support:
#: A reference to the active release branch location.
@: A reference to the binary storage root.
Args:
string: The configuration string to be transformed.
build_info: the current build information
Returns:
The adjusted file name string to be used in the manifest.
"""
if '#' in string:
string = string.replace('#', '%s/' % PathCompile(build_info))
if '@' in string:
string = string.replace('@', str(build_info.BinaryPath()))
return string
def PathCompile(build_info, file_name=None, base=None):
"""Compile the active path from the base path and the active conf path.
Attempt to do a reasonable job of joining path components with single
slashes.
The three main parts considered are the _base_url (or base arg), any
subdirectories from _conf_path, and the optional file name arg. These are
combined into [https://base.url][/conf/path/parts][/filename.ext]
We attempt to strip trailing slashes, so paths without a filename return
with no trailing /.
Args:
build_info: the current build information
file_name: append a filename to the path
base: use a non-default base path
Returns:
The compiled URL as a string.
"""
path = base
if not path:
path = build_info.ReleasePath()
path = path.rstrip('/')
sub_path = build_info.ActiveConfigPath()
if sub_path:
path += '/'
sub_path = '/'.join(sub_path).strip('/')
path += sub_path
if file_name:
path += '/'
file_name = file_name.lstrip('/')
path += file_name
return path
class DownloadError(Exception):
"""The transfer of the file failed."""
pass
class BaseDownloader(object):
"""Downloads files over HTTPS."""
def __init__(self, show_progress=False):
self._debug_info = {}
self._save_location = None
self._default_show_progress = show_progress
def _ConvertBytes(self, num_bytes):
"""Converts number of bytes to a human readable format.
Args:
num_bytes: The number to convert to a more human readable format (int).
Returns:
size: The number of bytes in human readable format (string).
"""
num_bytes = float(num_bytes)
if num_bytes >= 1099511627776:
terabytes = num_bytes / 1099511627776
size = '%.2fTB' % terabytes
elif num_bytes >= 1073741824:
gigabytes = num_bytes / 1073741824
size = '%.2fGB' % gigabytes
elif num_bytes >= 1048576:
megabytes = num_bytes / 1048576
size = '%.2fMB' % megabytes
elif num_bytes >= 1024:
kilobytes = num_bytes / 1024
size = '%.2fKB' % kilobytes
else:
size = '%.2fB' % num_bytes
return size
def _GetHandlers(self):
return [urllib2.HTTPSHandler()]
def _DownloadFile(self, url, max_retries=5, show_progress=None):
"""Downloads a file from and saves it to the specified location.
Args:
url: The address of the file to be downloaded.
max_retries: The number of times to attempt to download
a file if the first attempt fails.
show_progress: Print download progress to stdout (overrides default).
Raises:
DownloadError: The downloaded file did not match the expected file
size.
"""
attempt = 0
file_stream = None
opener = urllib2.OpenerDirector()
for handler in self._GetHandlers():
opener.add_handler(handler)
urllib2.install_opener(opener)
while True:
try:
attempt += 1
file_stream = urllib2.urlopen(url)
except urllib2.HTTPError:
logging.error('File not found on remote server: %s.', url)
except urllib2.URLError as e:
logging.error('Error connecting to remote server to download file '
'"%s". The error was: %s', url, e)
if file_stream:
if file_stream.getcode() in [200]:
break
else:
raise DownloadError('Invalid return code for file %s. [%d]' %
(url, file_stream.getcode()))
if attempt < max_retries:
logging.info('Sleeping for 20 seconds and then retrying the download.')
time.sleep(20)
else:
raise DownloadError('Permanent download failure for file %s.' % url)
self._StreamToDisk(file_stream, show_progress)
def DownloadFile(self, url, save_location, max_retries=5, show_progress=None):
"""Downloads a file to temporary storage.
Args:
url: The address of the file to be downloaded.
save_location: The full path of where the file should be saved.
max_retries: The number of times to attempt to download
a file if the first attempt fails.
show_progress: Print download progress to stdout (overrides default).
"""
self._save_location = save_location
self._DownloadFile(url, max_retries, show_progress)
def DownloadFileTemp(self, url, max_retries=5, show_progress=None):
"""Downloads a file to temporary storage.
Args:
url: The address of the file to be downloaded.
max_retries: The number of times to attempt to download
a file if the first attempt fails.
show_progress: Print download progress to stdout (overrides default).
Returns:
A string containing a path to the temporary file.
"""
destination = tempfile.NamedTemporaryFile()
self._save_location = destination.name
destination.close()
self._DownloadFile(url, max_retries, show_progress)
return self._save_location
def _DownloadChunkReport(self, bytes_so_far, total_size):
"""Prints download progress information.
Args:
bytes_so_far: The number of bytes downloaded so far.
total_size: The total size of the file being downloaded.
"""
percent = float(bytes_so_far) / total_size
percent = round(percent * 100, 2)
message = (('\rDownloaded %s of %s (%0.2f%%)' + (' ' * 10)) %
(self._ConvertBytes(bytes_so_far),
self._ConvertBytes(total_size), percent))
sys.stdout.write(message)
sys.stdout.flush()
if bytes_so_far >= total_size:
sys.stdout.write('\n')
def _StoreDebugInfo(self, file_stream, socket_error=None):
"""Gathers debug information for use when file downloads fail.
Args:
file_stream: The file stream object of the file being downloaded.
socket_error: Store the error raised from the socket class with
other debug info.
Returns:
debug_info: A dictionary containing various pieces of debugging
information.
"""
if socket_error:
self._debug_info['socket_error'] = socket_error
if file_stream:
for header in file_stream.info().header_items():
self._debug_info[header[0]] = header[1]
self._debug_info['current_time'] = time.strftime(
'%A, %d %B %Y %H:%M:%S UTC')
def PrintDebugInfo(self):
"""Print the debugging information to the screen."""
if self._debug_info:
print '\n\n\n\n'
print '---------------'
print 'Debugging info: '
print '---------------'
for key, value in self._debug_info.items():
print '%s: %s' % (key, value)
print '\n\n\n'
def _StreamToDisk(self, file_stream, show_progress=None):
"""Save a file stream to disk.
Args:
file_stream: The file stream returned by a successful urlopen()
show_progress: Print download progress to stdout (overrides default).
Raises:
DownloadError: Error retrieving file or saving to disk.
"""
progress = self._default_show_progress
if show_progress is not None:
progress = show_progress
bytes_so_far = 0
url = file_stream.geturl()
total_size = int(file_stream.info().getheader('Content-Length').strip())
try:
with open(self._save_location, 'wb') as output_file:
logging.info('Downloading file "%s" to "%s".', url, self._save_location)
while 1:
chunk = file_stream.read(CHUNK_BYTE_SIZE)
bytes_so_far += len(chunk)
if not chunk:
break
output_file.write(chunk)
if progress:
self._DownloadChunkReport(bytes_so_far, total_size)
except socket.error as e:
self._StoreDebugInfo(file_stream, str(e))
raise DownloadError('Socket error during download.')
except IOError:
raise DownloadError('File location could not be opened for writing: %s' %
self._save_location)
self._Validate(file_stream, total_size)
file_stream.close()
def _Validate(self, file_stream, expected_size):
"""Validate the downloaded file.
Args:
file_stream: The file stream returned by a successful urlopen()
expected_size: The total size of the file being downloaded.
Raises:
DownloadError: File failed validation.
"""
if not os.path.exists(self._save_location):
self._StoreDebugInfo(file_stream)
raise DownloadError('Could not locate file at %s' % self._save_location)
actual_file_size = os.path.getsize(self._save_location)
if actual_file_size != expected_size:
self._StoreDebugInfo(file_stream)
message = ('File size of %s bytes did not match expected size of %s!',
actual_file_size, expected_size)
raise DownloadError(message)
def VerifyShaHash(self, file_path, expected):
"""Verifies the SHA256 hash of a file.
Arguments:
file_path: The path to the file that will be checked.
expected: The expected SHA hash as a string.
Returns:
True if the calculated hash matches the expected hash.
False if the calculated hash does not match the expected hash or if there
was an error reading the file or the SHA file.
"""
sha_object = hashlib.new('sha256')
# Read the file in 4MB chunks to avoid running out of memory
# while processing very large files.
try:
with open(file_path, 'rb') as f:
while True:
current_chunk = f.read(4194304)
if not current_chunk:
break
sha_object.update(current_chunk)
except IOError:
logging.error('Unable to read file %s for SHA verification.', file_path)
return False
file_hash = sha_object.hexdigest()
expected = expected.lower()
if file_hash == expected:
logging.info('SHA256 hash for %s matched expected hash of %s.', file_path,
expected)
return True
else:
logging.error(
'SHA256 hash for %s was %s, which did not match expected hash of %s.',
file_path, file_hash, expected)
return False
# Set our downloader of choice
Download = BaseDownloader

226
lib/download_test.py Normal file
View File

@@ -0,0 +1,226 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.download."""
import StringIO
from fakefs import fake_filesystem
from glazier.lib import buildinfo
from glazier.lib import download
import mock
import unittest
_TEST_INI = """
[BUILD]
release=1.0
branch=stable
"""
class PathsTest(unittest.TestCase):
def setUp(self):
self.buildinfo = buildinfo.BuildInfo()
@mock.patch.object(buildinfo.BuildInfo, 'ReleasePath', autospec=True)
@mock.patch.object(buildinfo.BuildInfo, 'BinaryPath', autospec=True)
def testTransform(self, binpath, relpath):
relpath.return_value = 'https://glazier'
binpath.return_value = 'https://glazier/bin/'
result = download.Transform('stuff#blah', self.buildinfo)
self.assertEqual(result, 'stuffhttps://glazier/blah')
result = download.Transform('stuff@blah', self.buildinfo)
self.assertEqual(result, 'stuffhttps://glazier/bin/blah')
result = download.Transform('nothing _ here', self.buildinfo)
self.assertEqual(result, 'nothing _ here')
def testPathCompile(self):
result = download.PathCompile(
self.buildinfo, file_name='file.txt', base='/tmp/base')
self.assertEqual(result, '/tmp/base/file.txt')
self.buildinfo._active_conf_path = ['sub', 'dir']
result = download.PathCompile(
self.buildinfo, file_name='/file.txt', base='/tmp/base')
self.assertEqual(result, '/tmp/base/sub/dir/file.txt')
result = download.PathCompile(
self.buildinfo, file_name='file.txt', base='/tmp/base')
self.assertEqual(result, '/tmp/base/sub/dir/file.txt')
self.buildinfo._active_conf_path = ['sub', 'dir/other', 'another/']
result = download.PathCompile(
self.buildinfo, file_name='/file.txt', base='/tmp/')
self.assertEqual(result, '/tmp/sub/dir/other/another/file.txt')
class DownloadTest(unittest.TestCase):
def setUp(self):
self._dl = download.BaseDownloader()
# filesystem
self.filesystem = fake_filesystem.FakeFilesystem()
self.filesystem.CreateFile(r'C:\input.ini', contents=_TEST_INI)
download.os = fake_filesystem.FakeOsModule(self.filesystem)
download.open = fake_filesystem.FakeFileOpen(self.filesystem)
def testConvertBytes(self):
self.assertEqual(self._dl._ConvertBytes(123), '123.00B')
self.assertEqual(self._dl._ConvertBytes(23455), '22.91KB')
self.assertEqual(self._dl._ConvertBytes(3455555), '3.30MB')
self.assertEqual(self._dl._ConvertBytes(456555555), '435.41MB')
self.assertEqual(self._dl._ConvertBytes(56755555555), '52.86GB')
self.assertEqual(self._dl._ConvertBytes(6785555555555), '6.17TB')
@mock.patch.object(download.urllib2, 'urlopen', autospec=True)
@mock.patch.object(download.BaseDownloader, '_StreamToDisk', autospec=True)
@mock.patch.object(download.time, 'sleep', autospec=True)
@mock.patch.object(download.urllib2, 'HTTPSHandler', autospec=True)
def testDownloadFileInternal(self, cert_handler, sleep, stream, urlopen):
file_stream = mock.Mock()
file_stream.getcode.return_value = 200
httperr = download.urllib2.HTTPError('Error', None, None, None, None)
urlerr = download.urllib2.URLError('Error')
# 200
urlopen.side_effect = iter([httperr, urlerr, file_stream])
self._dl._DownloadFile('https://www.example.com/build.yaml', max_retries=4)
stream.assert_called_with(self._dl, file_stream, None)
self.assertTrue(cert_handler.called)
# 404
file_stream.getcode.return_value = 404
urlopen.side_effect = iter([httperr, file_stream])
self.assertRaises(download.DownloadError, self._dl._DownloadFile,
'https://www.example.com/build.yaml')
# retries
file_stream.getcode.return_value = 200
urlopen.side_effect = iter([httperr, httperr, file_stream])
self.assertRaises(
download.DownloadError,
self._dl._DownloadFile,
'https://www.example.com/build.yaml',
max_retries=2)
sleep.assert_has_calls([mock.call(20), mock.call(20)])
@mock.patch.object(download.BaseDownloader, '_DownloadFile', autospec=True)
def testDownloadFile(self, downf):
url = 'https://www.example.com/build.yaml'
path = r'C:\Cache\build.yaml'
self._dl.DownloadFile(url, path, max_retries=5)
downf.assert_called_with(self._dl, url, 5, None)
self.assertEqual(self._dl._save_location, path)
self._dl.DownloadFile(url, path, max_retries=5, show_progress=True)
downf.assert_called_with(self._dl, url, 5, True)
self._dl.DownloadFile(url, path, max_retries=5, show_progress=False)
downf.assert_called_with(self._dl, url, 5, False)
@mock.patch.object(download.BaseDownloader, '_DownloadFile', autospec=True)
@mock.patch.object(download.tempfile, 'NamedTemporaryFile', autospec=True)
def testDownloadFileTemp(self, tempf, downf):
url = 'https://www.example.com/build.yaml'
path = r'C:\Windows\Temp\tmpblahblah'
tempf.return_value.name = path
self._dl.DownloadFileTemp(url, max_retries=5)
downf.assert_called_with(self._dl, url, 5, None)
self.assertEqual(self._dl._save_location, path)
self._dl.DownloadFileTemp(url, max_retries=5, show_progress=True)
downf.assert_called_with(self._dl, url, 5, True)
self._dl.DownloadFileTemp(url, max_retries=5, show_progress=False)
downf.assert_called_with(self._dl, url, 5, False)
@mock.patch.object(download.BaseDownloader, '_StoreDebugInfo', autospec=True)
def testStreamToDisk(self, store_info):
# setup
http_stream = StringIO.StringIO()
http_stream.write('First line.\nSecond line.\n')
http_stream.seek(0)
download.CHUNK_BYTE_SIZE = 5
file_stream = mock.Mock()
file_stream.getcode.return_value = 200
file_stream.geturl.return_value = 'https://www.example.com/build.yaml'
file_stream.info.return_value.getheader.return_value = '25'
file_stream.read = http_stream.read
# success
self._dl._save_location = r'C:\download.txt'
self._dl._StreamToDisk(file_stream)
# Progress
with mock.patch.object(
self._dl, '_DownloadChunkReport', autospec=True) as report:
# default false
self._dl._default_show_progress = False
http_stream.seek(0)
self._dl._StreamToDisk(file_stream)
self.assertFalse(report.called)
# override true
http_stream.seek(0)
report.reset_mock()
self._dl._StreamToDisk(file_stream, show_progress=True)
self.assertTrue(report.called)
# default true
self._dl._default_show_progress = True
http_stream.seek(0)
report.reset_mock()
self._dl._StreamToDisk(file_stream)
self.assertTrue(report.called)
# override false
http_stream.seek(0)
report.reset_mock()
self._dl._StreamToDisk(file_stream, show_progress=False)
self.assertFalse(report.called)
# IOError
http_stream.seek(0)
self.filesystem.CreateDirectory(r'C:\Windows')
self._dl._save_location = r'C:\Windows'
self.assertRaises(download.DownloadError, self._dl._StreamToDisk,
file_stream)
# File Size
http_stream.seek(0)
file_stream.info.return_value.getheader.return_value = '100000'
self._dl._save_location = r'C:\download.txt'
self.assertRaises(download.DownloadError, self._dl._StreamToDisk,
file_stream)
# Socket Error
http_stream.seek(0)
file_stream.info.return_value.getheader.return_value = '25'
file_stream.read = mock.Mock(side_effect=download.socket.error('SocketErr'))
self.assertRaises(download.DownloadError, self._dl._StreamToDisk,
file_stream)
store_info.assert_called_with(self._dl, file_stream, 'SocketErr')
@mock.patch.object(download.BaseDownloader, '_StoreDebugInfo', autospec=True)
def testValidate(self, store_info):
file_stream = mock.Mock()
self._dl._save_location = r'C:\missing.txt'
self.assertRaises(download.DownloadError, self._dl._Validate, file_stream,
200)
store_info.assert_called_with(self._dl, file_stream)
def testVerifyShaHash(self):
test_sha256 = (
'58157BF41CE54731C0577F801035D47EC20ED16A954F10C29359B8ADEDCAE800')
# sha256
result = self._dl.VerifyShaHash(r'C:\input.ini', test_sha256)
self.assertTrue(result)
# missing source
result = self._dl.VerifyShaHash(r'C:\missing.ini', test_sha256)
self.assertFalse(result)
# missing hash
result = self._dl.VerifyShaHash(r'C:\input.ini', '')
self.assertFalse(result)
# mismatch hash
test_sha256 = (
'58157bf41ce54731c0577f801035d47ec20ed16a954f10c29359b8adedcae801')
result = self._dl.VerifyShaHash(r'C:\input.ini', test_sha256)
self.assertFalse(result)
if __name__ == '__main__':
unittest.main()

98
lib/drive_map.py Normal file
View File

@@ -0,0 +1,98 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Map network shares to drives on the local machine."""
import logging
import time
class ModuleImportError(Exception):
"""Error loading required python modules."""
class DriveMap(object):
"""Map and Unmap network shares."""
def __init__(self):
self._ModuleInit()
def MapDrive(self, drive_letter, server_path, username=None, password=None):
"""Maps a Samba or WebDAV path to a drive letter in Windows.
Args:
drive_letter: The drive letter to map the Samba path to.
server_path: The path to map to.
username: The username to use in mapping the drive.
password: The password to use in mapping the drive.
Returns:
False if drive map fails, True if drive map succeeds.
"""
wait = 1
limit = 65
while wait < limit:
try:
self._win32wnet.WNetAddConnection2(self._win32netcon.RESOURCETYPE_DISK,
drive_letter, server_path, None,
username, password, 0)
break
except self._win32wnet.error:
logging.error('Failed to map path %s to network drive %s.', server_path,
drive_letter)
logging.error('Waiting for %s seconds.', str(wait))
time.sleep(wait)
wait *= 2
if wait > limit:
logging.error('Unable to map path, aborting.')
return False
return True
def UnmapDrive(self, drive):
"""function to verify network drive connection.
Checks if drive is connected. Writes to temporary log if not connected.
Args:
drive: mapped network drive.
Returns:
False if no network drive connected. Returns True if drive unmaps.
"""
try:
self._win32wnet.WNetCancelConnection2(drive, 1, True)
except self._win32wnet.error:
logging.error('The network drive does not exist.')
return False
return True
def _ModuleInit(self):
"""Initialize win32 platform modules.
Raises:
ModuleImportError: failure to import a required module
"""
try:
import win32wnet # pylint: disable=g-import-not-at-top
self._win32wnet = win32wnet
except ImportError:
raise ModuleImportError('No win32wnet module available on this platform.')
try:
import win32netcon # pylint: disable=g-import-not-at-top
self._win32netcon = win32netcon
except ImportError:
raise ModuleImportError(
'No win32netcon module available on this platform.')

41
lib/file_util.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for working with files and directories."""
import logging
import os
import shutil
class Error(Exception):
pass
def CreateDirectories(path):
"""Create directory if the path to a file doesn't exist.
Args:
path: The full file path to where a file will be placed.
Raises:
Error: Failure creating the requested directory.
"""
dirname = os.path.dirname(path)
if not os.path.isdir(dirname):
logging.debug('Creating directory %s ', dirname)
try:
os.makedirs(dirname)
except (shutil.Error, OSError):
raise Error('Unable to make directory: %s' % dirname)

34
lib/file_util_test.py Normal file
View File

@@ -0,0 +1,34 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.file_util."""
from fakefs import fake_filesystem
from glazier.lib import file_util
import unittest
class FileUtilTest(unittest.TestCase):
def setUp(self):
self.filesystem = fake_filesystem.FakeFilesystem()
file_util.os = fake_filesystem.FakeOsModule(self.filesystem)
file_util.open = fake_filesystem.FakeFileOpen(self.filesystem)
def testCreateDirectories(self):
self.filesystem.CreateFile('/test')
self.assertRaises(file_util.Error, file_util.CreateDirectories,
'/test/file.txt')
file_util.CreateDirectories('/tmp/test/path/file.log')
self.assertTrue(self.filesystem.Exists('/tmp/test/path'))

78
lib/interact.py Normal file
View File

@@ -0,0 +1,78 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Glazier user interaction."""
import logging
import re
import time
def GetUsername():
"""Prompt the user for their username.
Returns:
The username string entered by the user.
"""
username = False
while not username:
username = Prompt('Please enter your username: ',
validator='^[a-zA-Z0-9]+$')
return username
def Keystroke(message, validator='.*', timeout=30):
"""Prompts the user for a keystroke and waits the specified amount of time.
Args:
message: the prompt message displayed to the user
validator: a regular expression to validate any responses
timeout: the length of time in seconds to wait for a response
Returns:
String of the character input from the user that matched input_regex.
"""
import msvcrt # pylint: disable=g-import-not-at-top
print message
i = 0
kbhit = False
while i < timeout and not kbhit:
kbhit = msvcrt.kbhit()
i += 1
time.sleep(1)
if kbhit:
response = msvcrt.getch()
result = re.match(validator, response)
if result:
logging.debug('Matched user input, %s, as a valid input.', response)
return response
logging.debug('No input from user prior to timeout.')
return None
def Prompt(message, validator='.*'):
"""Prompt the user for input.
Args:
message: the prompt message displayed to the user
validator: a regular expression to validate any responses
Returns:
a response string if successful, else None
"""
response = raw_input(message)
if not re.match(validator, response):
logging.error('Invalid response entered.')
return None
return response

59
lib/interact_test.py Normal file
View File

@@ -0,0 +1,59 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.interact."""
import sys
from glazier.lib import interact
import mock
import unittest
class InteractTest(unittest.TestCase):
@mock.patch('__builtin__.raw_input', autospec=True)
def testGetUsername(self, raw):
raw.side_effect = iter(['invalid-name', '', ' ', 'username1'])
self.assertEqual(interact.GetUsername(), 'username1')
@mock.patch.object(interact.time, 'sleep', autospec=True)
def testKeystroke(self, sleep):
msvcrt = mock.Mock()
msvcrt.kbhit.return_value = False
sys.modules['msvcrt'] = msvcrt
# no reply
result = interact.Keystroke('mesg', timeout=1)
self.assertEqual(result, None)
self.assertEqual(sleep.call_count, 1)
# reply
msvcrt.kbhit.side_effect = iter([False, False, False, False, True])
msvcrt.getch.return_value = 'v'
result = interact.Keystroke('mesg', timeout=100)
self.assertEqual(result, 'v')
self.assertEqual(sleep.call_count, 6)
# validation miss
msvcrt.kbhit.side_effect = iter([True])
result = interact.Keystroke('mesg', validator='[0-9]')
self.assertEqual(result, None)
@mock.patch('__builtin__.raw_input', autospec=True)
def testPrompt(self, raw):
raw.return_value = 'user*name'
result = interact.Prompt('mesg', '^\\w+$')
self.assertEqual(None, result)
result = interact.Prompt('mesg')
self.assertEqual('user*name', result)
if __name__ == '__main__':
unittest.main()

118
lib/log_copy.py Normal file
View File

@@ -0,0 +1,118 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This copies the build log around places."""
import datetime
import logging
import logging.handlers
import shutil
from glazier.lib import constants
from glazier.lib import drive_map
from glazier.lib import logs
from gwinpy.registry import registry
class LogCopyError(Exception):
pass
class LogCopyCredentials(object):
def __init__(self):
self._username = None
self._password = None
def GetUsername(self):
"""Override to provide share credentials."""
return self._username
def GetPassword(self):
"""Override to provide share credentials."""
return self._password
class LogCopy(object):
"""Copies text log files around."""
def __init__(self):
self._logging = logging.Logger('log_copy')
path = '%s\\log_copy.log' % logs.GetLogsPath()
self._logging.addHandler(logging.FileHandler(path))
def _EventLogUpload(self, source_log):
"""Upload the log file contents to the local EventLog."""
event_handler = logging.handlers.NTEventLogHandler('GlazierBuildLog')
logger = logging.Logger('eventlogger')
logger.addHandler(event_handler)
logger.setLevel(logging.INFO)
try:
with open(source_log, 'r') as f:
content = f.readlines()
for line in content:
logger.info(line)
except IOError:
raise LogCopyError(
'Unable to open log file. It will not be imported into '
'the Windows Event Log.')
def _GetLogFileName(self):
"""Creates the destination file name for a text log file.
Returns:
The full text file log name (string).
"""
reg = registry.Registry(root_key='HKLM')
hostname = reg.GetKeyValue(constants.REG_ROOT, 'name')
destination_file_date = datetime.datetime.utcnow().replace(microsecond=0)
destination_file_date = destination_file_date.isoformat()
destination_file_date = destination_file_date.replace(':', '')
return 'l:\\' + hostname + '-' + destination_file_date + '.log'
def _ShareUpload(self, source_log, share):
"""Copy the log file to a network file share.
Args:
source_log: Path to the source log file to be copied.
share: The destination share to copy the file to.
Raises:
LogCopyError: Failure to mount share and copy log.
"""
creds = LogCopyCredentials()
username = creds.GetUsername()
password = creds.GetPassword()
mapper = drive_map.DriveMap()
result = mapper.MapDrive('l:', share, username, password)
if result:
destination = self._GetLogFileName()
try:
shutil.copy(source_log, destination)
except shutil.Error:
raise LogCopyError('Log copy failed.')
mapper.UnmapDrive('l:')
else:
raise LogCopyError('Drive mapping failed.')
def EventLogCopy(self, source_log):
"""Copy a log flie to EventLog."""
self._EventLogUpload(source_log)
def ShareCopy(self, source_log, share):
"""Copy a log file via CIFS."""
self._ShareUpload(source_log, share)

95
lib/log_copy_test.py Normal file
View File

@@ -0,0 +1,95 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.log_copy."""
import datetime
import shutil
import sys
from glazier.lib import log_copy
import mock
import unittest
class LogCopyTest(unittest.TestCase):
@mock.patch.object(log_copy.logging, 'FileHandler', autospec=True)
def setUp(self, unused_handler):
self.log_file = r'C:\Windows\Logs\Glazier\glazier.log'
self.lc = log_copy.LogCopy()
# win32 modules
self.win32netcon = mock.Mock()
sys.modules['win32netcon'] = self.win32netcon
self.win32wnet = mock.Mock()
sys.modules['win32wnet'] = self.win32wnet
self._MockWinreg()
def _MockWinreg(self):
winreg = mock.Mock()
winreg.KEY_READ = 1
winreg.KEY_WRITE = 2
self.winreg = winreg
sys.modules['_winreg'] = self.winreg
def testGetLogFileName(self):
now = datetime.datetime.utcnow()
out_date = now.replace(microsecond=0).isoformat().replace(':', '')
self.winreg.QueryValueEx.return_value = ['WORKSTATION1-W']
with mock.patch.object(
log_copy.datetime, 'datetime', autospec=True) as mock_dt:
mock_dt.utcnow.return_value = now
result = self.lc._GetLogFileName()
self.assertEqual(result, r'l:\WORKSTATION1-W-' + out_date + '.log')
@mock.patch.object(log_copy.LogCopy, '_EventLogUpload', autospec=True)
def testEventLogCopy(self, event_up):
self.lc.EventLogCopy(self.log_file)
event_up.assert_called_with(self.lc, self.log_file)
@mock.patch.object(log_copy.LogCopy, '_GetLogFileName', autospec=True)
@mock.patch.object(log_copy.shutil, 'copy', autospec=True)
@mock.patch.object(log_copy.drive_map.DriveMap, 'UnmapDrive', autospec=True)
@mock.patch.object(log_copy.drive_map.DriveMap, 'MapDrive', autospec=True)
def testShareUpload(self, map_drive, unmap_drive, copy, get_file_name):
class TestCredProvider(log_copy.LogCopyCredentials):
def GetUsername(self):
return 'test_user'
def GetPassword(self):
return 'test_pass'
log_copy.LogCopyCredentials = TestCredProvider
log_host = 'log-host.example.com'
get_file_name.return_value = 'log.txt'
self.lc.ShareCopy(self.log_file, log_host)
map_drive.assert_called_with(mock.ANY, 'l:', 'log-host.example.com',
'test_user', 'test_pass')
copy.assert_called_with(self.log_file, 'log.txt')
unmap_drive.assert_called_with(mock.ANY, 'l:')
# map error
map_drive.return_value = None
self.assertRaises(log_copy.LogCopyError, self.lc.ShareCopy, self.log_file,
log_host)
# copy error
map_drive.return_value = True
copy.side_effect = shutil.Error()
self.assertRaises(log_copy.LogCopyError, self.lc.ShareCopy, self.log_file,
log_host)
if __name__ == '__main__':
unittest.main()

58
lib/logs.py Normal file
View File

@@ -0,0 +1,58 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Set up logging for all imaging tools."""
import logging
import logging.handlers
from glazier.lib import constants
class LogError(Exception):
pass
def GetLogsPath():
path = constants.SYS_LOGS_PATH
if constants.FLAGS.environment == 'WinPE':
path = constants.WINPE_LOGS_PATH
return path
def Setup():
"""Sets up the logging environment."""
log_file = '%s\\%s' % (GetLogsPath(), constants.BUILD_LOG_FILE)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# file
try:
fh = logging.FileHandler(log_file)
except IOError:
raise LogError('Failed to open log file %s.', log_file)
formatter = logging.Formatter(
'%(asctime)s.%(msecs)03d\t%(filename)s:%(lineno)d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
fh.setFormatter(formatter)
fh.setLevel(logging.DEBUG)
# console
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# add the handlers to the logger
logger.addHandler(fh)
logger.addHandler(ch)

37
lib/logs_test.py Normal file
View File

@@ -0,0 +1,37 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.logs."""
from glazier.lib import logs
import mock
import unittest
class LoggingTest(unittest.TestCase):
@mock.patch.object(logs.logging, 'FileHandler')
def testSetup(self, fh):
logs.constants.FLAGS.environment = 'Host'
logs.Setup()
fh.assert_called_with('%s\\glazier.log' % logs.constants.SYS_LOGS_PATH)
logs.constants.FLAGS.environment = 'WinPE'
logs.Setup()
fh.assert_called_with('X:\\glazier.log')
fh.side_effect = IOError
self.assertRaises(logs.LogError, logs.Setup)
if __name__ == '__main__':
unittest.main()

67
lib/ntp.py Normal file
View File

@@ -0,0 +1,67 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Glazier interface to NTP time service."""
import logging
import socket
import subprocess
import time
from glazier.lib.constants import WINPE_SYSTEM32
import ntplib
RETRY_DELAY = 30
class NtpException(Exception):
pass
def SyncClockToNtp(retries=2, server='time.google.com'):
"""Syncs the hardware clock to an NTP server."""
logging.info('Reading time from NTP server %s.', server)
attempts = 0
client = ntplib.NTPClient()
response = None
while True:
try:
response = client.request(server, version=3)
except (ntplib.NTPException, socket.gaierror) as e:
logging.error('NTP client request error: %s', str(e))
if response or attempts >= retries:
break
logging.info(
'Unable to contact NTP server %s to sync machine clock. This '
'machine may not have an IP address yet; waiting %d seconds and '
'trying again. Repeated failure may indicate network or driver '
'problems.', server, RETRY_DELAY)
time.sleep(RETRY_DELAY)
attempts += 1
if not response:
raise NtpException('No response from NTP server.')
local_time = time.localtime(response.ref_time)
current_date = time.strftime('%m-%d-%Y', local_time)
current_time = time.strftime('%H:%M:%S', local_time)
logging.info('Current date/time is %s %s', current_date, current_time)
date_set = r'%s\cmd.exe /c date %s' % (WINPE_SYSTEM32, current_date)
result = subprocess.call(date_set, shell=True)
logging.info('Setting date returned result %s', result)
time_set = r'%s\cmd.exe /c time %s' % (WINPE_SYSTEM32, current_time)
result = subprocess.call(time_set, shell=True)
logging.info('Setting time returned result %s', result)

53
lib/ntp_test.py Normal file
View File

@@ -0,0 +1,53 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.ntp."""
from glazier.lib import ntp
import mock
import unittest
class NtpTest(unittest.TestCase):
@mock.patch.object(ntp.time, 'sleep', autospec=True)
@mock.patch.object(ntp.subprocess, 'call', autospec=True)
@mock.patch.object(ntp.ntplib.NTPClient, 'request', autospec=True)
def testSyncClockToNtp(self, request, subproc, sleep):
return_time = mock.Mock()
return_time.ref_time = 1453220630.64458
request.side_effect = iter([None, None, None, return_time])
subproc.return_value = True
# Too Few Retries
self.assertRaises(ntp.NtpException, ntp.SyncClockToNtp)
sleep.assert_has_calls([mock.call(30), mock.call(30)])
# Sufficient Retries
ntp.SyncClockToNtp(retries=3, server='time.google.com')
request.assert_called_with(mock.ANY, 'time.google.com', version=3)
subproc.assert_has_calls([
mock.call(
r'X:\Windows\System32\cmd.exe /c date 01-19-2016', shell=True),
mock.call(
r'X:\Windows\System32\cmd.exe /c time 08:23:50', shell=True)
])
# Socket Error
request.side_effect = ntp.socket.gaierror
self.assertRaises(ntp.NtpException, ntp.SyncClockToNtp)
# NTP lib error
request.side_effect = ntp.ntplib.NTPException
self.assertRaises(ntp.NtpException, ntp.SyncClockToNtp)
if __name__ == '__main__':
unittest.main()

31
lib/policies/README.md Normal file
View File

@@ -0,0 +1,31 @@
# Glazier Installer Policies
[TOC]
Policy modules determine whether or not Autobuild should be allowed to proceed
with an installation.
## Usage
Each module should inherit from BasePolicy, and will receive a BuildInfo
instance (self.\_build_info).
If a policy fails, the module should raise ImagingPolicyException with a message
explaining the cause of failure. This will abort the build.
If a policy causes a warning, the module should raise ImagingPolicyWarning. This
will not abort the build, but may present the warning to the user.
## Modules
### DeviceModel
DeviceModel checks whether the local device is a supported hardware model. If
the device is not fully supported (outside tier1), the user is prompted whether
or not to abort the build.
### DiskEncryption
DiskEncryption checks whether encryption is required of the host, and if so,
whether the host is capable of encryption (TPM is present).

28
lib/policies/__init__.py Normal file
View File

@@ -0,0 +1,28 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Simplify access to Glazier policy modules."""
from glazier.lib.policies import base
from glazier.lib.policies import device_model
from glazier.lib.policies import disk_encryption
# pylint: disable=invalid-name
BannedPlatform = device_model.BannedPlatform
DeviceModel = device_model.DeviceModel
DiskEncryption = disk_encryption.DiskEncryption
ImagingPolicyException = base.ImagingPolicyException
ImagingPolicyWarning = base.ImagingPolicyWarning

35
lib/policies/base.py Normal file
View File

@@ -0,0 +1,35 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generic imaging policy class."""
class ImagingPolicyException(Exception):
"""Policy verification failed with a fatal condition."""
pass
class ImagingPolicyWarning(Exception):
"""Policy verification failed with a non-fatal condition."""
pass
class BasePolicy(object):
def __init__(self, build_info):
self._build_info = build_info
def Verify(self):
"""Override this function to implement a new policy."""
pass

29
lib/policies/base_test.py Normal file
View File

@@ -0,0 +1,29 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.policies.base."""
from glazier.lib.policies import base
import unittest
class BaseTest(unittest.TestCase):
def testVerify(self):
b = base.BasePolicy(None)
b.Verify()
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,92 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Ensure the device hardware is supported."""
from __future__ import print_function
import logging
import re
from glazier.lib.policies.base import BasePolicy
from glazier.lib.policies.base import ImagingPolicyException
_PARTIAL_NOTICE = ("""
!!!!! Notice !!!!!
The installer considers this hardware model obsolete or experimental (%s).
The hardware you are using is not part of active inventory.
While the installer may support this device, it is not being tested for
compatibility. There is a chance you may experience problems imaging.
We recommend considering a hardware refresh before continuing.
""")
_UNSUPPORTED_NOTICE = ("""
!!!!! Warning !!!!!
The installer does not recognize this hardware model (%s).
If you chose to continue, this will be an unsupported build. The
final install MAY BE BROKEN. You should only continue if you are
sure you know what you're doing. When in doubt, contact support
for assistance.
""")
class DeviceModel(BasePolicy):
"""Verify that the device hardware is supported."""
def _ModelSupportPrompt(self, message, this_model):
"""Prompts the user whether to halt an unsupported build.
Args:
message: A message to be displayed to the user.
this_model: The hardware model that failed validation.
Returns:
true if the user wishes to proceed anyway, else false.
"""
warning = message % this_model
print(warning)
answer = raw_input('Do you still want to proceed (y/n)? ')
answer_re = r'^[Yy](es)?$'
if re.match(answer_re, answer):
return True
return False
def Verify(self):
model = self._build_info.ComputerModel()
logging.debug('Verifying hardware support tier for %s.', model)
tier = self._build_info.SupportTier()
if tier == 1:
return True
build_anyway = False
if tier == 2:
build_anyway = self._ModelSupportPrompt(_PARTIAL_NOTICE, model)
else:
build_anyway = self._ModelSupportPrompt(_UNSUPPORTED_NOTICE, model)
if not build_anyway:
raise ImagingPolicyException(
'User chose not to continue with current model.')
logging.info('User chose to continue with partial or unsupported build.')
return build_anyway
class BannedPlatform(BasePolicy):
def Verify(self):
raise ImagingPolicyException(
'Windows cannot be installed on this platform.')

View File

@@ -0,0 +1,61 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.policies.device_model."""
from glazier.lib.policies import device_model
import mock
import unittest
class DeviceModelTest(unittest.TestCase):
@mock.patch('__builtin__.raw_input', autospec=True)
@mock.patch('__builtin__.print', autospec=True)
@mock.patch('glazier.lib.buildinfo.BuildInfo',
autospec=True)
def testVerify(self, build_info, user_out, user_in):
dm = device_model.DeviceModel(build_info)
# Tier1
dm._build_info.SupportTier.return_value = 1
self.assertTrue(dm.Verify())
# Tier 2
user_in.return_value = 'yes'
dm._build_info.ComputerModel.return_value = 'Test Workstation'
dm._build_info.SupportTier.return_value = 2
self.assertTrue(dm.Verify())
user_out.assert_called_with(device_model._PARTIAL_NOTICE %
'Test Workstation')
# Unsupported: Continue
user_in.return_value = 'Y'
dm._build_info.SupportTier.return_value = 0
self.assertTrue(dm.Verify())
user_out.assert_called_with(device_model._UNSUPPORTED_NOTICE %
'Test Workstation')
# Unsupported: Abort
user_in.return_value = 'n'
self.assertRaises(device_model.ImagingPolicyException, dm.Verify)
@mock.patch('glazier.lib.buildinfo.BuildInfo',
autospec=True)
def testBannedPlatform(self, build_info):
bp = device_model.BannedPlatform(build_info)
self.assertRaises(device_model.ImagingPolicyException,
bp.Verify)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,27 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Ensure the machine supports disk encryption if required."""
from glazier.lib.policies.base import BasePolicy
from glazier.lib.policies.base import ImagingPolicyException
class DiskEncryption(BasePolicy):
def Verify(self):
level = self._build_info.EncryptionLevel()
if level == 'tpm' and not self._build_info.TpmPresent():
raise ImagingPolicyException(
'This machine requires a TPM for encryption but no TPM was found.')

View File

@@ -0,0 +1,40 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.policies.disk_encryption."""
from glazier.lib.policies import disk_encryption
import mock
import unittest
class DiskEncryptionTest(unittest.TestCase):
@mock.patch(
'glazier.lib.buildinfo.BuildInfo', autospec=True)
def testVerify(self, build_info):
de = disk_encryption.DiskEncryption(build_info)
de._build_info.EncryptionLevel.return_value = 'none'
de._build_info.TpmPresent.return_value = True
de.Verify()
de._build_info.EncryptionLevel.return_value = 'startupkey'
de.Verify()
de._build_info.EncryptionLevel.return_value = 'tpm'
de.Verify()
de._build_info.TpmPresent.return_value = False
self.assertRaises(disk_encryption.ImagingPolicyException, de.Verify)
if __name__ == '__main__':
unittest.main()

49
lib/power.py Normal file
View File

@@ -0,0 +1,49 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Turn things on and off."""
import subprocess
from glazier.lib import constants
def _System32():
if constants.FLAGS.environment == 'WinPE':
return constants.WINPE_SYSTEM32
else:
return constants.SYS_SYSTEM32
def Shutdown(timeout, reason):
"""Shuts down a Windows machine, given a timeout period and a reason.
Args:
timeout: How long to wait before shutting down the machine.
reason: Reason why the machine is being shut down. This will be displayed
to the user and written to the Windows event log.
"""
subprocess.call(r'%s\shutdown.exe -s -t %s -c "%s" -f'
% (_System32(), timeout, reason))
def Restart(timeout, reason):
"""Restarts a Windows machine, given a timeout period and a reason.
Args:
timeout: How long to wait before restarting the machine.
reason: Reason why the machine is being restarted. This will be displayed
to the user and written to the Windows event log.
"""
subprocess.call(r'%s\shutdown.exe -r -t %s -c "%s" -f'
% (_System32(), timeout, reason))

38
lib/power_test.py Normal file
View File

@@ -0,0 +1,38 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.power."""
from glazier.lib import power
import mock
import unittest
class PowerTest(unittest.TestCase):
@mock.patch.object(power.subprocess, 'call', autospec=True)
def testRestart(self, call):
power.Restart(60, 'Reboot fixes everything.')
call.assert_called_with('C:\\Windows\\System32\\shutdown.exe -r -t 60 '
'-c "Reboot fixes everything." -f')
@mock.patch.object(power.subprocess, 'call', autospec=True)
def testShutdown(self, call):
power.Shutdown(30, 'Because I said so.')
call.assert_called_with('C:\\Windows\\System32\\shutdown.exe -s -t 30 '
'-c "Because I said so." -f')
if __name__ == '__main__':
unittest.main()

151
lib/powershell.py Normal file
View File

@@ -0,0 +1,151 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Run scripts with Windows Powershell."""
import logging
import os
import subprocess
from glazier.lib import constants
from glazier.lib import resources
class PowerShellError(Exception):
pass
def _Powershell():
if constants.FLAGS.environment == 'WinPE':
return constants.WINPE_POWERSHELL
else:
return constants.SYS_POWERSHELL
class PowerShell(object):
"""Interact with the powershell interpreter to run scripts."""
def __init__(self, echo_off=False):
self.echo_off = echo_off
def _LaunchPs(self, op, args, ok_result):
"""Launch the powershell executable to run a script.
Args:
op: -Command or -File
args: any additional commandline args as a list
ok_result: a list of acceptable exit codes; default is 0
Raises:
PowerShellError: failure to execute powershell command cleanly
"""
if op not in ['-Command', '-File']:
raise PowerShellError('Unsupported operation type. [%s]' % op)
if not ok_result:
ok_result = [0]
cmd = [_Powershell(), '-NoProfile', '-NoLogo', op] + args
if not self.echo_off:
logging.debug('Running Powershell:%s', cmd)
result = subprocess.call(cmd, shell=True)
if result not in ok_result:
raise PowerShellError('Powershell command returned non-zero.\n%s' % cmd)
def RunCommand(self, command, ok_result=None):
"""Run a powershell script on the local filesystem.
Args:
command: a list containing the command and all accompanying arguments
ok_result: a list of acceptable exit codes; default is 0
"""
assert isinstance(command, list), 'command must be passed as a list'
if ok_result:
assert isinstance(ok_result,
list), 'result codes must be passed as a list'
self._LaunchPs('-Command', command, ok_result)
def _GetResPath(self, path):
"""Translate an installer resource path into a local path.
Args:
path: the resource path string
Raises:
PowerShellError: unable to locate the requested resource
Returns:
The local filesystem path as a string.
"""
r = resources.Resources()
try:
path = r.GetResourceFileName(path)
except resources.FileNotFound as e:
raise PowerShellError(e)
return os.path.normpath(path)
def RunResource(self, path, args=None, ok_result=None):
"""Run a Powershell script supplied as an installer resource file.
Args:
path: relative path to a script under the installer resources directory
args: a list of any optional powershell arguments
ok_result: a list of acceptable exit codes; default is 0
"""
path = self._GetResPath(path)
if not args:
args = []
else:
assert isinstance(args, list), 'args must be passed as a list'
if ok_result:
assert isinstance(ok_result,
list), 'result codes must be passed as a list'
self.RunLocal(path, args, ok_result)
def RunLocal(self, path, args=None, ok_result=None):
"""Run a powershell script on the local filesystem.
Args:
path: a local filesystem path string
args: a list of any optional powershell arguments
ok_result: a list of acceptable exit codes; default is 0
Raises:
PowerShellError: Invalid path supplied for execution.
"""
if not os.path.exists(path):
raise PowerShellError('Cannot find path to script. [%s]' % path)
if not args:
args = []
else:
assert isinstance(args, list), 'args must be passed as a list'
if ok_result:
assert isinstance(ok_result,
list), 'result codes must be passed as a list'
self._LaunchPs('-File', [path] + args, ok_result)
def SetExecutionPolicy(self, policy):
"""Set the shell execution policy.
Args:
policy: One of Restricted, RemoteSigned, AllSigned, Unrestricted
Raises:
PowerShellError: Attempting to set an unsupported policy.
"""
if policy not in ['Restricted', 'RemoteSigned', 'AllSigned', 'Unrestricted'
]:
raise PowerShellError('Unknown execution policy: %s' % policy)
self.RunCommand(['Set-ExecutionPolicy', '-ExecutionPolicy', policy])
def StartShell(self):
"""Start the PowerShell interpreter."""
subprocess.call([_Powershell(), '-NoProfile', '-NoLogo'], shell=True)

101
lib/powershell_test.py Normal file
View File

@@ -0,0 +1,101 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.powershell."""
from fakefs import fake_filesystem
from glazier.lib import powershell
import mock
import unittest
class PowershellTest(unittest.TestCase):
def setUp(self):
self.fs = fake_filesystem.FakeFilesystem()
powershell.os = fake_filesystem.FakeOsModule(self.fs)
powershell.resources.os = fake_filesystem.FakeOsModule(self.fs)
self.fs.CreateFile('/resources/bin/script.ps1')
self.ps = powershell.PowerShell()
@mock.patch.object(powershell.subprocess, 'call', autospec=True)
def testRunLocal(self, call):
args = ['-Arg1', '-Arg2']
call.return_value = 0
with self.assertRaises(powershell.PowerShellError):
self.ps.RunLocal('/resources/missing.ps1', args=args)
self.ps.RunLocal('/resources/bin/script.ps1', args=args)
cmd = [
powershell._Powershell(), '-NoProfile', '-NoLogo', '-File',
'/resources/bin/script.ps1', '-Arg1', '-Arg2'
]
call.assert_called_with(cmd, shell=True)
with self.assertRaises(powershell.PowerShellError):
self.ps.RunLocal('/resources/bin/script.ps1', args=args, ok_result=[100])
@mock.patch.object(powershell.subprocess, 'call', autospec=True)
def testRunCommand(self, call):
call.return_value = 0
self.ps.RunCommand(['Get-ChildItem', '-Recurse'])
cmd = [
powershell._Powershell(), '-NoProfile', '-NoLogo', '-Command',
'Get-ChildItem', '-Recurse'
]
call.assert_called_with(cmd, shell=True)
with self.assertRaises(powershell.PowerShellError):
self.ps.RunCommand(['Get-ChildItem', '-Recurse'], ok_result=[100])
@mock.patch.object(powershell.PowerShell, '_LaunchPs', autospec=True)
def testRunResource(self, launch):
self.ps.RunResource('bin/script.ps1', args=['>>', 'out.txt'], ok_result=[0])
launch.assert_called_with = '/resources/bin/script.ps1'
# Not Found
self.assertRaises(powershell.PowerShellError, self.ps.RunResource,
'missing.ps1')
# Validation
self.assertRaises(
AssertionError,
self.ps.RunResource,
'bin/script.ps1',
args='not a list')
self.assertRaises(
AssertionError,
self.ps.RunResource,
'bin/script.ps1',
args=[],
ok_result='0')
@mock.patch.object(powershell.subprocess, 'call', autospec=True)
def testSetExecutionPolicy(self, call):
call.return_value = 0
self.ps.SetExecutionPolicy(policy='RemoteSigned')
call.assert_called_with(
[
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
'-NoProfile', '-NoLogo', '-Command', 'Set-ExecutionPolicy',
'-ExecutionPolicy', 'RemoteSigned'
],
shell=True)
with self.assertRaisesRegexp(powershell.PowerShellError,
'Unknown execution policy.*'):
self.ps.SetExecutionPolicy(policy='RandomPolicy')
@mock.patch.object(powershell.subprocess, 'call', autospec=True)
def testStartShell(self, unused_call):
self.ps.StartShell()
if __name__ == '__main__':
unittest.main()

58
lib/resources.py Normal file
View File

@@ -0,0 +1,58 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provide access to non-Python installer resource files."""
import os
from glazier.lib import constants
import gflags as flags
FLAGS = flags.FLAGS
flags.DEFINE_string('resource_path', '',
'Path to top level installer resource file storage.')
class FileNotFound(Exception):
pass
class Resources(object):
def __init__(self, resource_dir=None):
self._path = resource_dir
if not self._path:
self._path = constants.FLAGS.resource_path
if not self._path:
path = os.path.dirname(os.path.realpath(__file__))
self._path = os.path.join(path, 'resources')
def GetResourceFileName(self, file_name):
"""Returns the full path to a resource file.
Args:
file_name: A file to search for under the installer resource directory.
Returns:
The full path to the resource on disk.
Raises:
FileNotFound: No file exists at the determined path.
"""
file_name = file_name.strip('/')
path = os.path.join(self._path, file_name)
if os.path.exists(path):
return path
raise FileNotFound('Could not locate a resource with path %s.' % path)

40
lib/resources_test.py Normal file
View File

@@ -0,0 +1,40 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.resources."""
from fakefs import fake_filesystem
from glazier.lib import resources
import mock
import unittest
class ResourcesTest(unittest.TestCase):
def setUp(self):
self.fs = fake_filesystem.FakeFilesystem()
resources.os = fake_filesystem.FakeOsModule(self.fs)
self.fs.CreateFile('/test/file.txt')
def testGetResourceFileName(self):
r = resources.Resources('/test')
self.assertRaises(resources.FileNotFound, r.GetResourceFileName,
'missing.txt')
self.assertEqual(r.GetResourceFileName('file.txt'), '/test/file.txt')
with mock.patch.object(r.os, 'cwd') as cwd:
cwd.return_value = '/test2'
r = resources.Resources()
self.assertEqual(
r.GetResourceFileName('file.txt'), '/test2/resources/file.txt')

7
lib/spec/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Glazier Installer Host Specification
The spec module contains libraries for determining the desired host
specification:
* Desired operating system
* Desired host name

13
lib/spec/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

41
lib/spec/flags.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Class for determining host spec via flags."""
import gflags as flags
FLAGS = flags.FLAGS
flags.DEFINE_string('glazier_spec_hostname', '',
'Host name for this installation.')
flags.DEFINE_string('glazier_spec_fqdn', '',
'Host FQDN for this installation.')
flags.DEFINE_string('glazier_spec_os', '',
'Operating system code for this image.')
def GetOs():
"""Get the desired OS via flags."""
return FLAGS.glazier_spec_os
def GetFqdn():
"""Get the desired FQDN via flags."""
return FLAGS.glazier_spec_fqdn
def GetHostname():
"""Get the desired hostname via flags."""
return FLAGS.glazier_spec_hostname

40
lib/spec/spec.py Normal file
View File

@@ -0,0 +1,40 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generic class for determining the desired host operating system."""
from glazier.lib.spec import flags as flag_spec
import gflags as flags
SPEC_OPTS = {
'flag': flag_spec,
}
FLAGS = flags.FLAGS
flags.DEFINE_enum(
'glazier_spec', None,
SPEC_OPTS.keys(),
('Which host specification module to use for determining host features '
'like Hostname and OS.'))
class UnknownSpec(Exception):
pass
def GetModule():
try:
return SPEC_OPTS[FLAGS.glazier_spec]
except KeyError:
raise UnknownSpec(FLAGS.glazier_spec)

67
lib/timers.py Normal file
View File

@@ -0,0 +1,67 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Store points in time to be used for metrics."""
import datetime
class Timers(object):
"""Store named time elements."""
def __init__(self):
self._time_store = {}
def Get(self, name):
"""Get the stored value of a single timer.
Args:
name: The name of the timer being requested.
Returns:
A specific named datetime value if stored, or None
"""
if name in self._time_store:
return self._time_store[name]
return None
def GetAll(self):
"""Get the dictionary of all stored timers.
Returns:
A dictionary of all stored timer names and values.
"""
return self._time_store
def Now(self):
"""Get the current time using the default timer method.
Returns:
A datetime object.
"""
return datetime.datetime.utcnow()
def Set(self, name, at_time=None):
"""Set a timer at a specific time.
Defaults to the current time in UTC.
Args:
name: Name of the timer being set.
at_time: A predetermined time value to store.
"""
if at_time:
self._time_store[name] = at_time
else:
self._time_store[name] = self.Now()

44
lib/timers_test.py Normal file
View File

@@ -0,0 +1,44 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for glazier.lib.timers."""
import datetime
from glazier.lib import timers
import mock
import unittest
class TimersTest(unittest.TestCase):
def setUp(self):
self.t = timers.Timers()
@mock.patch.object(timers.datetime, 'datetime', autospec=True)
def testNow(self, dt):
now = datetime.datetime.utcnow()
dt.utcnow.return_value = now
self.assertEqual(self.t.Now(), now)
def testGetAll(self):
time_2 = datetime.datetime.now()
self.t.Set('timer_1')
self.t.Set('timer_2', at_time=time_2)
self.assertEqual(self.t.Get('timer_2'), time_2)
all_t = self.t.GetAll()
self.assertIn('timer_1', all_t)
self.assertIn('timer_2', all_t)
if __name__ == '__main__':
unittest.main()

64
lib/timezone.py Normal file
View File

@@ -0,0 +1,64 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Timezone processing for Windows.
> Resource Requirements
* resources/cldr/common/supplemental/windowsZones.xml
The Windows timezone map from http://cldr.unicode.org.
"""
import logging
from xml.dom.minidom import parse
from glazier.lib import resources
import gflags as flags
FLAGS = flags.FLAGS
RESOURCE_PATH = 'cldr/common/supplemental/windowsZones.xml'
flags.DEFINE_string('windows_zones_resource', RESOURCE_PATH,
'Timezone map file location.')
class TimezoneError(Exception):
pass
class Timezone(object):
"""Timezone processing for Windows."""
def __init__(self, load_map=False):
self.zones = {}
if load_map:
self.LoadMap()
def LoadMap(self):
res = resources.Resources()
try:
win_zones = parse(res.GetResourceFileName(FLAGS.windows_zones_resource))
except resources.FileNotFound:
raise TimezoneError('Cannot load zone map from %s.' %
FLAGS.windows_zones_resource)
for zone in win_zones.getElementsByTagName('mapZone'):
self.zones[zone.getAttribute('type')] = zone.getAttribute('other')
def TranslateZone(self, name):
found = None
try:
found = self.zones[name]
except KeyError:
logging.error('Unable to translate zone %s.', name)
return found

42
lib/timezone_test.py Normal file
View File

@@ -0,0 +1,42 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the timezone library."""
from glazier.lib import timezone
import unittest
class TimezoneTest(unittest.TestCase):
def setUp(self):
self.tz = timezone.Timezone()
timezone.FLAGS.windows_zones_resource = timezone.RESOURCE_PATH
self.tz.LoadMap()
def testLoadMap(self):
timezone.FLAGS.windows_zones_resource = '/no/such/file.xml'
self.assertRaises(timezone.TimezoneError, self.tz.LoadMap)
def testTranslateZone(self):
zone = self.tz.TranslateZone('Pacific/Tahiti')
self.assertEqual(zone, 'Hawaiian Standard Time')
zone = self.tz.TranslateZone('Nonsense/Atlantis')
self.assertEqual(zone, None)
zone = self.tz.TranslateZone('Europe/Dublin')
self.assertEqual(zone, 'GMT Standard Time')
if __name__ == '__main__':
unittest.main()