mirror of
https://github.com/google/glazier.git
synced 2025-12-19 18:27:35 -05:00
Initial commit of Glazier project code.
This commit is contained in:
25
CONTRIBUTING.md
Normal file
25
CONTRIBUTING.md
Normal 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
202
LICENSE
Normal 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.
|
||||
41
README.md
41
README.md
@@ -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
14
__init__.py
Normal 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
90
autobuild.py
Normal 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
71
autobuild_test.py
Normal 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
13
chooser/__init__.py
Normal 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
136
chooser/chooser.py
Normal 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
161
chooser/chooser_test.py
Normal 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
135
chooser/fields.py
Normal 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
102
chooser/fields_test.py
Normal 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
12
doc/index.md
Normal 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
41
doc/setup/index.md
Normal 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
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
98
doc/yaml/chooser_ui.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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
186
doc/yaml/index.md
Normal 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
26
doc/yaml/tips.md
Normal 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
13
lib/__init__.py
Normal 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
409
lib/actions/README.md
Normal 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
72
lib/actions/__init__.py
Normal 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
60
lib/actions/abort.py
Normal 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
62
lib/actions/abort_test.py
Normal 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
95
lib/actions/base.py
Normal 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
30
lib/actions/base_test.py
Normal 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
41
lib/actions/domain.py
Normal 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])
|
||||
57
lib/actions/domain_test.py
Normal file
57
lib/actions/domain_test.py
Normal 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
134
lib/actions/drivers.py
Normal 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
101
lib/actions/drivers_test.py
Normal 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
109
lib/actions/file_system.py
Normal 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)
|
||||
114
lib/actions/file_system_test.py
Normal file
114
lib/actions/file_system_test.py
Normal 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
147
lib/actions/files.py
Normal 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
216
lib/actions/files_test.py
Normal 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
172
lib/actions/installer.py
Normal 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)
|
||||
209
lib/actions/installer_test.py
Normal file
209
lib/actions/installer_test.py
Normal 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
53
lib/actions/powershell.py
Normal 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)
|
||||
62
lib/actions/powershell_test.py
Normal file
62
lib/actions/powershell_test.py
Normal 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
41
lib/actions/registry.py
Normal 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))
|
||||
53
lib/actions/registry_test.py
Normal file
53
lib/actions/registry_test.py
Normal 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
89
lib/actions/sysprep.py
Normal 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
134
lib/actions/sysprep_test.py
Normal 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
60
lib/actions/system.py
Normal 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)
|
||||
77
lib/actions/system_test.py
Normal file
77
lib/actions/system_test.py
Normal 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
27
lib/actions/timers.py
Normal 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)
|
||||
45
lib/actions/timers_test.py
Normal file
45
lib/actions/timers_test.py
Normal 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
38
lib/actions/tpm.py
Normal 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
48
lib/actions/tpm_test.py
Normal 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
104
lib/actions/updates.py
Normal 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))
|
||||
92
lib/actions/updates_test.py
Normal file
92
lib/actions/updates_test.py
Normal 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
74
lib/bitlocker.py
Normal 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
59
lib/bitlocker_test.py
Normal 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
582
lib/buildinfo.py
Normal 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
497
lib/buildinfo_test.py
Normal 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
94
lib/cache.py
Normal 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
86
lib/cache_test.py
Normal 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
13
lib/config/__init__.py
Normal 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
62
lib/config/base.py
Normal 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
45
lib/config/base_test.py
Normal 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
165
lib/config/builder.py
Normal 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))
|
||||
90
lib/config/builder_test.py
Normal file
90
lib/config/builder_test.py
Normal 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
82
lib/config/files.py
Normal 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
66
lib/config/files_test.py
Normal 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
97
lib/config/runner.py
Normal 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
128
lib/config/runner_test.py
Normal 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
61
lib/constants.py
Normal 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
129
lib/domain_join.py
Normal 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
367
lib/download.py
Normal 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
226
lib/download_test.py
Normal 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
98
lib/drive_map.py
Normal 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
41
lib/file_util.py
Normal 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
34
lib/file_util_test.py
Normal 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
78
lib/interact.py
Normal 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
59
lib/interact_test.py
Normal 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
118
lib/log_copy.py
Normal 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
95
lib/log_copy_test.py
Normal 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
58
lib/logs.py
Normal 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
37
lib/logs_test.py
Normal 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
67
lib/ntp.py
Normal 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
53
lib/ntp_test.py
Normal 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
31
lib/policies/README.md
Normal 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
28
lib/policies/__init__.py
Normal 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
35
lib/policies/base.py
Normal 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
29
lib/policies/base_test.py
Normal 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()
|
||||
92
lib/policies/device_model.py
Normal file
92
lib/policies/device_model.py
Normal 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.')
|
||||
61
lib/policies/device_model_test.py
Normal file
61
lib/policies/device_model_test.py
Normal 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()
|
||||
27
lib/policies/disk_encryption.py
Normal file
27
lib/policies/disk_encryption.py
Normal 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.')
|
||||
40
lib/policies/disk_encryption_test.py
Normal file
40
lib/policies/disk_encryption_test.py
Normal 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
49
lib/power.py
Normal 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
38
lib/power_test.py
Normal 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
151
lib/powershell.py
Normal 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
101
lib/powershell_test.py
Normal 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
58
lib/resources.py
Normal 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
40
lib/resources_test.py
Normal 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
7
lib/spec/README.md
Normal 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
13
lib/spec/__init__.py
Normal 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
41
lib/spec/flags.py
Normal 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
40
lib/spec/spec.py
Normal 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
67
lib/timers.py
Normal 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
44
lib/timers_test.py
Normal 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
64
lib/timezone.py
Normal 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
42
lib/timezone_test.py
Normal 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()
|
||||
Reference in New Issue
Block a user