├── .gitignore ├── .travis.yml ├── COPYING ├── INSTALL ├── MANIFEST.in ├── Makefile ├── README.md ├── etc └── simian │ └── settings.cfg ├── gae_resources ├── client_resources │ ├── customize_client.md │ └── site_default.zip └── icons │ └── icons.md ├── postinstall ├── preinstall ├── roots.pem ├── setup.cfg ├── setup.py ├── src ├── simian │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── base.py │ │ ├── client.py │ │ ├── gaeserver.py │ │ ├── tlslite_bridge.py │ │ ├── util.py │ │ └── x509.py │ ├── client │ │ ├── __init__.py │ │ ├── client.py │ │ └── gae_client.zip │ ├── mac │ │ ├── __init__.py │ │ ├── admin │ │ │ ├── __init__.py │ │ │ ├── acl_groups.py │ │ │ ├── applesus.py │ │ │ ├── applesus_update_names.py │ │ │ ├── broken_clients.py │ │ │ ├── config.py │ │ │ ├── css │ │ │ │ ├── base.css │ │ │ │ ├── forms.css │ │ │ │ ├── goog.css │ │ │ │ ├── layout.css │ │ │ │ ├── main.css │ │ │ │ ├── self_report.css │ │ │ │ ├── simian.css │ │ │ │ └── widgets.css │ │ │ ├── custom_filters.py │ │ │ ├── groups.py │ │ │ ├── host.py │ │ │ ├── ip_blacklist.py │ │ │ ├── js │ │ │ │ ├── forms.js │ │ │ │ ├── groups.js │ │ │ │ ├── main.js │ │ │ │ ├── menu.js │ │ │ │ ├── net.js │ │ │ │ └── tags.js │ │ │ ├── lock_admin.py │ │ │ ├── main.py │ │ │ ├── maintenance.py │ │ │ ├── manifest_modifications.py │ │ │ ├── misc.py │ │ │ ├── package.py │ │ │ ├── package_alias.py │ │ │ ├── packages.py │ │ │ ├── panic.py │ │ │ ├── release_report.py │ │ │ ├── static │ │ │ │ ├── bubble_point.png │ │ │ │ ├── bubble_point_down.png │ │ │ │ ├── bubble_point_left.png │ │ │ │ ├── check_no_box.png │ │ │ │ ├── check_no_box_white.png │ │ │ │ ├── disclosure_arrow.png │ │ │ │ ├── disclosure_arrow_back.png │ │ │ │ ├── disclosure_arrow_down.png │ │ │ │ ├── dl.gif │ │ │ │ ├── favicon.ico │ │ │ │ ├── file.png │ │ │ │ ├── info.png │ │ │ │ ├── loading_222.gif │ │ │ │ ├── menutoggle.png │ │ │ │ ├── pinyellow.png │ │ │ │ ├── restart.png │ │ │ │ ├── search.svg │ │ │ │ ├── simian.png │ │ │ │ ├── simian.svg │ │ │ │ ├── simian_header.png │ │ │ │ ├── trash_white.png │ │ │ │ ├── uploadicon.png │ │ │ │ ├── uploadicon_dark.png │ │ │ │ └── x.png │ │ │ ├── summary.py │ │ │ ├── tags.py │ │ │ ├── templates │ │ │ │ ├── acl_groups.html │ │ │ │ ├── applesus_installs.html │ │ │ │ ├── applesus_installs_list.html │ │ │ │ ├── applesus_list.html │ │ │ │ ├── applesus_log.html │ │ │ │ ├── base.html │ │ │ │ ├── broken_clients.html │ │ │ │ ├── config.html │ │ │ │ ├── error.html │ │ │ │ ├── groups.html │ │ │ │ ├── host.html │ │ │ │ ├── host_popup.html │ │ │ │ ├── install_problems.html │ │ │ │ ├── install_problems_list.html │ │ │ │ ├── installs.html │ │ │ │ ├── installs_list.html │ │ │ │ ├── list_edit.html │ │ │ │ ├── lock_admin.html │ │ │ │ ├── manifest_modifications.html │ │ │ │ ├── msu_log_summary.html │ │ │ │ ├── msu_log_summary_list.html │ │ │ │ ├── package.html │ │ │ │ ├── package_alias.html │ │ │ │ ├── package_logs.html │ │ │ │ ├── packages.html │ │ │ │ ├── panic.html │ │ │ │ ├── panic_set_verify.html │ │ │ │ ├── plist.html │ │ │ │ ├── preflight_exits.html │ │ │ │ ├── preflight_exits_list.html │ │ │ │ ├── release_report.html │ │ │ │ ├── results_pagination.html │ │ │ │ ├── summary.html │ │ │ │ ├── tags.html │ │ │ │ ├── upload_pkg_form.html │ │ │ │ └── user_settings.html │ │ │ ├── upload_icon.py │ │ │ ├── uploadpkg.py │ │ │ └── xsrf.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── dynamic_manifest.py │ │ │ ├── groups.py │ │ │ ├── info.py │ │ │ ├── packages.py │ │ │ └── urls.py │ │ ├── app.yaml │ │ ├── client │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ ├── flight_common.py │ │ │ ├── network_detect.py │ │ │ ├── postflight.py │ │ │ ├── preflight.py │ │ │ ├── report_broken_client.py │ │ │ └── version.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── applesus.py │ │ │ ├── auth.py │ │ │ ├── compress.py │ │ │ ├── datastore_locks.py │ │ │ ├── gae_util.py │ │ │ ├── hw.py │ │ │ ├── ipcalc.py │ │ │ ├── mail.py │ │ │ ├── retry.py │ │ │ └── util.py │ │ ├── cron.yaml │ │ ├── cron │ │ │ ├── __init__.py │ │ │ ├── applesus.py │ │ │ ├── main.py │ │ │ ├── maintenance.py │ │ │ └── reports_cache.py │ │ ├── index.yaml │ │ ├── main.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── constants.py │ │ │ ├── munki.py │ │ │ ├── properties.py │ │ │ └── settings.py │ │ ├── munki │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── handlers │ │ │ │ ├── __init__.py │ │ │ │ ├── applesus.py │ │ │ │ ├── auth.py │ │ │ │ ├── catalogs.py │ │ │ │ ├── icons.py │ │ │ │ ├── manifests.py │ │ │ │ ├── pkgs.py │ │ │ │ ├── pkgsinfo.py │ │ │ │ ├── reports.py │ │ │ │ ├── uauth.py │ │ │ │ └── uploadfile.py │ │ │ ├── pkgs.py │ │ │ └── plist.py │ │ ├── queue.yaml │ │ └── urls.py │ ├── munki │ │ ├── __init__.py │ │ ├── postflight │ │ ├── preflight │ │ ├── report_broken_client │ │ ├── simian_client.py │ │ └── version.py │ ├── settings.py │ ├── stubs.py │ └── util │ │ ├── __init__.py │ │ ├── appid_generator.py │ │ ├── compile_js.py │ │ ├── create_gae_bundle.sh │ │ ├── link_module.sh │ │ ├── simianfacter │ │ └── validate_settings.py └── tests │ ├── __init__.py │ ├── appenginesdk.py │ └── simian │ ├── __init__.py │ ├── auth │ ├── __init__.py │ ├── base_medium_test.py │ ├── base_test.py │ ├── client_test.py │ ├── gaeserver_test.py │ ├── util_test.py │ ├── x509_medium_test.py │ └── x509_test.py │ ├── client │ ├── __init__.py │ └── client_test.py │ ├── mac │ ├── __init__.py │ ├── admin │ │ ├── __init__.py │ │ ├── acl_groups_test.py │ │ ├── applesus_test.py │ │ ├── base_handler_test.py │ │ ├── broken_clients_test.py │ │ ├── groups_test.py │ │ ├── host_test.py │ │ ├── ip_blacklist_test.py │ │ ├── manifest_modifications_test.py │ │ ├── package_alias_test.py │ │ ├── package_test.py │ │ ├── panic_test.py │ │ ├── release_report_test.py │ │ ├── summary_test.py │ │ ├── tags_test.py │ │ └── upload_icon_test.py │ ├── api │ │ ├── __init__.py │ │ ├── dynamic_manifest_test.py │ │ ├── groups_test.py │ │ ├── info_test.py │ │ └── packages.py │ ├── client │ │ ├── __init__.py │ │ ├── client_test.py │ │ ├── munkicommon_mock.py │ │ ├── postflight_test.py │ │ └── preflight_test.py │ ├── common │ │ ├── __init__.py │ │ ├── applesus_test.py │ │ ├── auth_test.py │ │ ├── compress_test.py │ │ ├── gae_util_test.py │ │ ├── hw_test.py │ │ ├── ipcalc_test.py │ │ ├── test.py │ │ ├── test_base.py │ │ ├── testdata │ │ │ ├── 041-0120.English.dist │ │ │ ├── applesus.sucatalog │ │ │ ├── system_profiler.xml │ │ │ └── testpackage.plist │ │ ├── util_medium_test.py │ │ └── util_test.py │ ├── cron │ │ ├── __init__.py │ │ ├── applesus_test.py │ │ ├── maintenance_test.py │ │ └── reports_cache_test.py │ ├── models │ │ ├── __init__.py │ │ ├── base_test.py │ │ ├── munki_test.py │ │ └── settings_test.py │ ├── munki │ │ ├── __init__.py │ │ ├── common_test.py │ │ ├── handlers │ │ │ ├── __init__.py │ │ │ ├── __init___test.py │ │ │ ├── applesus_test.py │ │ │ ├── auth_test.py │ │ │ ├── catalogs_test.py │ │ │ ├── icons_test.py │ │ │ ├── manifests_medium_test.py │ │ │ ├── manifests_test.py │ │ │ ├── pkgs_test.py │ │ │ ├── pkgsinfo_test.py │ │ │ ├── reports_test.py │ │ │ ├── uauth_test.py │ │ │ └── uploadfile_test.py │ │ ├── pkgs_test.py │ │ └── plist_test.py │ └── urls_test.py │ ├── settings_datastore_test.py │ ├── settings_medium_test.py │ ├── settings_test.py │ └── test_settings.py └── tgz2dmg.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cdo] 2 | *.egg 3 | *.tar 4 | *.tgz 5 | .eggs 6 | gae_bundle 7 | src/simian.egg-info/ 8 | src/simian/mac/admin/js/simian.js 9 | test 10 | tmpcontents 11 | tmppkgs 12 | VE 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | script: make test 3 | sudo: required 4 | os: 5 | - osx 6 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Simian 2 | App Engine server and client enhancements for Munki client 3 | 4 | 5 | OVERVIEW 6 | 7 | The machine you untarred this distribution onto can become your 8 | administrative workstation. From here you can setup configuration, push 9 | config and code to your App Engine instance, and build .DMGs to install on 10 | your clients. 11 | 12 | 13 | PREREQUISITES 14 | 15 | App Engine SDK 16 | ------------- 17 | Portions of the App Engine SDK are supplied in zips for importing where 18 | necessary. 19 | 20 | src/tests/simian/auth/gae_server.zip supplies the App Engine SDK so that all 21 | unit tests may run without requiring the SDK. To update this zip, download and 22 | zip the "google" folder within the latest Linux App Engine SDK. 23 | 24 | However, you will need the full App Engine SDK so that you can upload the 25 | Simian site to your App Engine instance. 26 | 27 | For information on downloading and installing the SDK, see: 28 | https://cloud.google.com/appengine/downloads 29 | 30 | 31 | Other packages 32 | -------------- 33 | google-apputils-python 34 | M2Crypto 35 | python_dateutil 36 | pyyaml 37 | 38 | These should be resolved automatically by setuptools, however if you have 39 | trouble, run easy_install -U modulename. 40 | 41 | 42 | TESTING 43 | 44 | Before installation, run the unit tests to make sure that your Python is 45 | working correctly. Run: 46 | 47 | make test 48 | 49 | For more information about testing, see: 50 | https://github.com/google/simian/wiki/Testing 51 | 52 | 53 | INSTALLATION 54 | 55 | Installation can be broken up into five tasks: 56 | 57 | * Configuring your installation 58 | * Pushing the server code to your App Engine instance 59 | * Rolling a .DMG to install on clients, based on your configuration 60 | 61 | For information on configuration, see: 62 | https://github.com/google/simian/wiki/Configuration 63 | 64 | For information on pushing your server code to your AppEngine instance, see: 65 | https://github.com/google/simian/wiki/AdminSetup 66 | 67 | Briefly, to roll your own .DMG after configuring, run: 68 | make dmg 69 | 70 | 71 | DEVELOPMENT 72 | 73 | To join in on the development with others, see: 74 | https://github.com/google/simian 75 | 76 | To make patches yourself, it's advised to edit the code in this directory 77 | under src/, and run "make test" to run unit tests before releasing. 78 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/simian/settings.py 2 | recursive-exclude src/simian/mac/admin * 3 | recursive-exclude src/simian/mac/common * 4 | include src/simian/mac/common/__init__.py 5 | include src/simian/mac/common/hw.py 6 | recursive-exclude src/simian/mac/cron * 7 | recursive-exclude src/simian/mac/munki/handlers * 8 | recursive-exclude src/simian/mac/api * 9 | exclude src/simian/mac/munki/common.py 10 | exclude src/simian/mac/urls.py 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Status: Archived 2 | 3 | This repository has been archived and is [no longer maintained](https://groups.google.com/forum/#!topic/simian-discuss/IoG626TXjPM). 4 | 5 | Simian has served Google well for many years, so we’d love to see development continue with someone else as maintainer. 6 | If you’re interested in taking Simian over, please fork the project. 7 | 8 | ### Overview 9 | 10 | **Note 1: OS X 10.6, 10.7, 10.8 support dropped (22 April 2016)** 11 | 12 | **Note 2: Simian now supports Munki2 categories!** 13 | 14 | Simian is an enterprise-class Mac OS X software deployment solution hosted on Google App Engine (why [App Engine](../../wiki/AppEngineAtAGlance)?). It scales to any enterprise fleet size automatically, and offers a future proof client extended from the active [Munki open-source project](https://github.com/munki/munki). 15 | 16 | Because the Simian server is live code, not a static file server, it offers unique benefits over a typical Munki+Apache deployment: 17 | * Simian can generate software manifests dynamically, on the fly, based on client properties. 18 | * The administration overhead of static manifest management is removed. 19 | * Any possible customizations to client software offerings are possible by modifying the server. 20 | 21 | In summary, Simian's feature set allows it to: 22 | * Deploy new or updated software by targeting a single Mac or tens of thousands. 23 | * Deploy updates whether the Mac is on an internal network/VPN or not. 24 | * Force unattended/background installation of some packages, while allowing others to be optional. 25 | * Forcefully deploy security patches and reboot Macs after a given date, with varying levels of warning notifications. 26 | * Tightly manage Apple Software Update catalogs and update release, or let updates auto-promote automatically. 27 | * Dynamically target clients based on user, hostname, os version, group (tag), and more. 28 | * Obtain reports on all of this and the fleet overall. 29 | 30 | Much of this and more is due to the outstanding work of Greg Neagle and the Munki community. To read more about the other features Munki offers please visit the [Munki on GitHub](https://github.com/munki/munki). 31 | 32 | ### Getting Started 33 | 34 | Please refer to the [Wiki documentation](../../wiki/AdminSetup) for instructions on how to download, configure, build, and deploy Simian. 35 | 36 | For screenshots and an overview of Simian features, please see the [SimianAtAGlance wiki](../../wiki/SimianAtAGlance). 37 | 38 | ### License 39 | 40 | The work done under Simian has been licensed under Apache License 2.0. The license file can be found 41 | [here](https://github.com/google/simian/blob/master/COPYING). You can find out more about the license, at 42 | 43 | http://www.apache.org/licenses/LICENSE-2.0 44 | 45 | ### Contact 46 | 47 | If you have any issue or question which isn't covered in the Wiki documentation, we recommend you search and then post to the [simian-discuss@googlegroups.com Google Group](https://groups.google.com/forum/#!forum/simian-discuss). 48 | 49 | If your question is sensitive in nature, feel free to reach direct to the engineering team at simian-eng@googlegroups.com. 50 | 51 | Finally, if you'd like to stay aware of future Simian news, please subscribe to [simian-announce@googlegroups.com Google Group](https://groups.google.com/forum/#!forum/simian-announce) 52 | 53 | -------------------------------------------------------------------------------- /etc/simian/settings.cfg: -------------------------------------------------------------------------------- 1 | # Simian Configuration File. 2 | # 3 | # This file must live on client machines at /etc/simian/settings.cfg 4 | # 5 | # For more information, please visit the following Wiki page: 6 | # http://code.google.com/p/simian/wiki/Configuration 7 | # 8 | 9 | [settings] 10 | 11 | # This should match your App Engine Application ID. 12 | # For example, if your app is http://example.appspot.com this is "example" 13 | subdomain = example 14 | 15 | # Do not change unless you're using a Google Apps Domain with App Engine. 16 | domain = appspot.com 17 | 18 | # The full DN of the CA that Simian certificates are signed by. 19 | required_issuer = CN=ca-server.example.com 20 | 21 | # Root CA Cert chain PEM path; default is provided with Simian. 22 | root_ca_cert_chain_pem = /usr/local/munki/simian/roots.pem 23 | 24 | # Path to directory of client certificates (client public cert and private key). 25 | # If you're using Puppet for configuration management, this can be set to: 26 | # /etc/puppet/ssl/ 27 | # 28 | # This directory should contain two subdirectories: 29 | # ./certs/.pem 30 | # ./private_keys/.pem 31 | client_ssl_path = /etc/simian/ssl/ 32 | 33 | # Domain for simianadmin uauth; set to your Google Apps Domain if not gmail. 34 | auth_domain = gmail.com 35 | 36 | # Enable Apple SUS integration. 37 | applesus = true 38 | 39 | 40 | # Variables to supplement Puppet's facter. 41 | # Any variables not returned by facter will be populated from the values here. 42 | # For deployments lacking Puppet/facter, these will be used exhaustively. 43 | 44 | # Used for reporting purposes only. 45 | configtrack = stable 46 | 47 | # Used for "canarying" package deployment / deploying in phases. 48 | simiantrack = stable 49 | 50 | # If unset, the newest valid certificate in /etc/simian/ssl/certs will be used. 51 | certname = 52 | 53 | # If unuset, the most common value in `last -100` will be used. 54 | primary_user = 55 | 56 | # If unset, SystemConfiguration.SCDynamicStoreCopyComputerName() will be used. 57 | hostname = 58 | 59 | # Shorthand site code, such as NYC, SFO, etc. 60 | site = 61 | -------------------------------------------------------------------------------- /gae_resources/client_resources/customize_client.md: -------------------------------------------------------------------------------- 1 | Zip files for client customization go here. 2 | 3 | -=WARNING=- 4 | The .zip files stored here is a publicly accessible, widely known resource. 5 | Only enable client_resources if you need it and are prepared to mitigate abuse. 6 | See https://github.com/munki/munki/wiki/Client-Customization 7 | (The client_resources static_dir must be uncommented in src/simian/mac/app.yaml to enable this feature.) -------------------------------------------------------------------------------- /gae_resources/client_resources/site_default.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/gae_resources/client_resources/site_default.zip -------------------------------------------------------------------------------- /gae_resources/icons/icons.md: -------------------------------------------------------------------------------- 1 | The icons directory has potential for abuse. 2 | Static icons stored in gae_resources/icons will be served from here. 3 | See https://github.com/munki/munki/wiki/Product-Icons -------------------------------------------------------------------------------- /preinstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | simian_files=("/Applications/Managed Software Center.app") 3 | 4 | for file in "${simian_files[@]}"; do 5 | if [ -e "$file" ]; then 6 | rm -rf "$file" 7 | fi 8 | done -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [easy_install] 2 | zip_ok = 0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Do NOT change the above sha-bang line unless if you know what you are doing. 18 | # 19 | # 20 | # Licensed under the Apache License, Version 2.0 (the "License"); 21 | # you may not use this file except in compliance with the License. 22 | # You may obtain a copy of the License at 23 | # 24 | # http://www.apache.org/licenses/LICENSE-2.0 25 | # 26 | # Unless required by applicable law or agreed to in writing, software 27 | # distributed under the License is distributed on an "AS-IS" BASIS, 28 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | # See the License for the specific language governing permissions and 30 | # limitations under the License. 31 | # # 32 | 33 | try: 34 | from setuptools import setup, find_packages 35 | except ImportError: 36 | print 'required Python setuptools is missing.' 37 | print 'install from http://pypi.python.org/pypi/setuptools' 38 | raise SystemExit(1) 39 | 40 | 41 | REQUIRE_BASE = [ 42 | 'setuptools>=18.2', 43 | 'pyasn1==0.4.2', 44 | 'pyasn1_modules==0.2.2', 45 | 'tlslite==0.4.9', 46 | 'requests==2.14.2', 47 | 'GoogleAppEngineCloudStorageClient', 48 | ] 49 | 50 | REQUIRE_SETUP = REQUIRE_BASE + [ 51 | 'google_apputils==0.4', # 0.4.1: ezsetup broken, 0.4.2: testbase broken 52 | 'python-dateutil>=1.4,<2', # because of google_apputils 53 | 'python-gflags==2.0', # gflags 3.0+ requires python2.7 54 | ] 55 | 56 | REQUIRE_TEST = REQUIRE_BASE + [ 57 | 'mock', 58 | 'mox>=0.5.3', 59 | 'pyyaml>=3.10', 60 | 'Pillow', # needed for google_apputils init_all_stubs() (for imging stub) 61 | 'pyfakefs', 62 | 'unittest2', 63 | 'webapp2', 64 | 'webtest', 65 | 'WebOb>=1.2', # webtest requires >=1.2. 66 | ] 67 | 68 | REQUIRE_INSTALL = REQUIRE_BASE 69 | 70 | SIMIAN_STUBS = [ 71 | ('simian_preflight', 'RunSimianPreflight'), 72 | ('simian_postflight', 'RunSimianPostflight'), 73 | ] 74 | SIMIAN_ENTRY_POINTS = ['%s = simian.stubs:%s' % s for s in SIMIAN_STUBS] 75 | 76 | setup( 77 | name = 'simian', 78 | version = '2.5', 79 | url = 'https://github.com/google/simian', 80 | license = 'Apache 2.0', 81 | description = 'An App Engine-based client & server component for Munki', 82 | author = 'Google', 83 | author_email = 'simian-eng@googlegroups.com', 84 | 85 | packages = find_packages('src', exclude=['tests']), 86 | package_dir = {'': 'src'}, 87 | package_data = { 88 | '': ['*.zip'], 89 | }, 90 | include_package_data = True, 91 | 92 | entry_points = { 93 | 'console_scripts': SIMIAN_ENTRY_POINTS, 94 | }, 95 | 96 | setup_requires = REQUIRE_SETUP, 97 | install_requires = REQUIRE_INSTALL, 98 | tests_require = REQUIRE_TEST, 99 | 100 | google_test_dir = 'src/tests', 101 | ) 102 | -------------------------------------------------------------------------------- /src/simian/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/auth/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2012 Google Inc. All Rights Reserved. 4 | # 5 | 6 | # Name of the cookie set by server 7 | AUTH_TOKEN_COOKIE = 'Auth1Token' 8 | -------------------------------------------------------------------------------- /src/simian/auth/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | 19 | """Client module. Contains classes to handle authentication and 20 | authorization for Munki clients acting against Simian server. 21 | 22 | Classes: 23 | 24 | AuthSessionSimianClient: Session storage for Simian clients 25 | AuthSimianClient: Simian client Auth class 26 | """ 27 | 28 | 29 | 30 | 31 | from simian.auth import base 32 | from simian.auth import util 33 | 34 | 35 | class Error(Exception): 36 | """Base""" 37 | 38 | 39 | class CaParametersError(Error): 40 | """Error obtaining CA parameters.""" 41 | 42 | 43 | class AuthSessionSimianClient(base.Auth1ClientSession): 44 | """AuthSession data container used for Simian Auth client.""" 45 | 46 | 47 | class AuthSimianClient(base.Auth1Client): 48 | """Auth1 client which uses AuthSessionSimianClient for session storage.""" 49 | 50 | def __init__(self): 51 | super(AuthSimianClient, self).__init__() 52 | 53 | def LoadCaParameters(self, settings): 54 | """Load ca/cert parameters for default CA. 55 | 56 | Args: 57 | settings: object with attribute level access to settings. 58 | Raises: 59 | CaParametersError: if any errors occur loading keys/certs. 60 | """ 61 | try: 62 | ca_params = util.GetCaParameters(settings, omit_server_private_key=True) 63 | except util.CaParametersError, e: 64 | raise CaParametersError(*e.args) 65 | self._ca_pem = ca_params.ca_public_cert_pem 66 | self._server_cert_pem = ca_params.server_public_cert_pem 67 | self._required_issuer = ca_params.required_issuer 68 | 69 | def GetSessionClass(self): 70 | return AuthSessionSimianClient 71 | -------------------------------------------------------------------------------- /src/simian/auth/tlslite_bridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Bridge to support tlslite 0.4.* and 0.3.8.""" 18 | 19 | import array 20 | import logging 21 | import os 22 | import sys 23 | 24 | # pylint: disable=unused-import,g-import-not-at-top 25 | try: 26 | from tlslite.X509 import X509 27 | _RETURN_ARRAY = True 28 | except ImportError: 29 | # tlslite 0.4.0+ 30 | # __init__ in tlslite imports all avaliable api. 31 | # part of it relies on fcntl which is not avaliable on appengine. 32 | # we don't use this api, so safely stub out fcntl for appengine 33 | _RETURN_ARRAY = False 34 | if (os.environ.get('SERVER_SOFTWARE', '').startswith('Google App Engine') or 35 | os.environ.get('SERVER_SOFTWARE', '').startswith('Development')): 36 | logging.warning('stub out fcntl') 37 | sys.modules['fcntl'] = 1 38 | from tlslite.x509 import X509 39 | 40 | from tlslite.utils.keyfactory import parsePEMKey 41 | 42 | 43 | def StrToArray(s): 44 | """Return an array of bytes or bytearray for a string. 45 | 46 | Return type depends on tlslite version 47 | Args: 48 | s: str 49 | Returns: 50 | array.array/bytearray instance 51 | """ 52 | if _RETURN_ARRAY: 53 | return array.array('B', s) 54 | return bytearray(s) 55 | -------------------------------------------------------------------------------- /src/simian/client/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/client/gae_client.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/client/gae_client.zip -------------------------------------------------------------------------------- /src/simian/mac/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/mac/admin/acl_groups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """ACL Groups admin handler.""" 18 | import httplib 19 | import re 20 | from simian.mac import admin 21 | from simian.mac import models 22 | from simian.mac.common import auth 23 | from simian.mac.common import util 24 | 25 | # TODO(user): consolidate email regex duplicated in settings. 26 | MAIL_REGEX = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}\b' 27 | 28 | 29 | class ACLGroups(admin.AdminHandler): 30 | 31 | def get(self, group=None): 32 | """GET handler.""" 33 | if not self.IsAdminUser(): 34 | self.error(httplib.FORBIDDEN) 35 | return 36 | 37 | if group: 38 | group_members = self.GetMembers(group) 39 | d = {'report_type': 'acl_groups', 'list': [(gm,) for gm in group_members], 40 | 'columns': 1, 'regex': [r'/%s/' % MAIL_REGEX], 41 | 'title': 'ACL Group: %s' % group, 'back': '/admin/acl_groups', 42 | 'infopanel': 'Full email address required (e.g. user@example.com)'} 43 | self.Render('list_edit.html', d) 44 | else: 45 | group_data = [] 46 | for name, title in auth.ACL_GROUPS.items(): 47 | members = self.GetMembers(name) 48 | group_data.append({'name': name, 'title': title, 'members': members}) 49 | d = {'report_type': 'acl_groups', 'groups': group_data} 50 | self.Render('acl_groups.html', d) 51 | 52 | @admin.AdminHandler.XsrfProtected('acl_groups') 53 | def post(self, group=None): 54 | """POST handler.""" 55 | if not self.IsAdminUser(): 56 | self.error(httplib.FORBIDDEN) 57 | return 58 | 59 | if group: 60 | values = self.request.get_all('item_0', None) 61 | members = [] 62 | is_email = re.compile(MAIL_REGEX) 63 | for member in values: 64 | if is_email.match(member): 65 | members.append(member) 66 | else: 67 | self.error(httplib.BAD_REQUEST) 68 | self.response.out.write('malformed email') 69 | return 70 | models.KeyValueCache.MemcacheWrappedSet(group, 'text_value', 71 | util.Serialize(members)) 72 | self.redirect('/admin/acl_groups?msg=Group%20saved') 73 | 74 | def GetMembers(self, group_name): 75 | """Get a list of members belonging to the group.""" 76 | members = models.KeyValueCache.MemcacheWrappedGet(group_name, 'text_value') 77 | member_list = [] 78 | if members: 79 | try: 80 | member_list = util.Deserialize(members) 81 | except util.DeserializeError: 82 | pass 83 | return member_list 84 | -------------------------------------------------------------------------------- /src/simian/mac/admin/css/base.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Base elements and general style. 5 | * 6 | */ 7 | 8 | body { 9 | margin: 0; 10 | padding: 0 0 150px 0; 11 | font-family: "Arial", "Helvetica", sans-serif; 12 | font-size: .8125em; 13 | background-color: #FFF; 14 | color: #222; 15 | font-size: 13px; 16 | white-space-collapsing: discard; 17 | } 18 | 19 | a { 20 | color: #1155CC; 21 | text-decoration: none; 22 | cursor: pointer; 23 | } 24 | 25 | h1, h2, h3, h4, h2 a, .title2 { 26 | display: inline; 27 | font-family: "Arial", "Helvetica", sans-serif; 28 | font-size: 16px; 29 | color: #222222; 30 | text-decoration: none; 31 | font-style: normal; 32 | font-weight: normal; 33 | } 34 | 35 | h3 { 36 | font-size: 14px; 37 | } 38 | 39 | h4 { 40 | font-size: 13px; 41 | } 42 | 43 | 44 | .tt { 45 | font-family: "Courier New", "Courier", monospace; 46 | font-size: 1.2em; 47 | } 48 | 49 | li { 50 | margin: .2em 0; 51 | } 52 | 53 | .count { 54 | color: #666; 55 | font-size: 0.9em; 56 | } 57 | 58 | .success { 59 | font-weight: bold; 60 | color: #090; 61 | } 62 | 63 | .note { 64 | font-size: 0.8em; 65 | color: #999; 66 | } 67 | .note2 { 68 | color: #666; 69 | font-size: 0.9em; 70 | } 71 | 72 | table a.anchor { 73 | color: #777; 74 | text-decoration: none; 75 | font-size: 13px; 76 | visibility: hidden; 77 | } 78 | table a.anchor:hover { 79 | background-color: #AAA; 80 | } 81 | table tr:hover .anchor { 82 | visibility: visible; 83 | } 84 | -------------------------------------------------------------------------------- /src/simian/mac/admin/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | @import url('layout.css'); 3 | @import url('forms.css'); 4 | @import url('widgets.css'); 5 | @import url('simian.css'); 6 | @import url('goog.css'); 7 | -------------------------------------------------------------------------------- /src/simian/mac/admin/css/self_report.css: -------------------------------------------------------------------------------- 1 | header { 2 | display: none; 3 | } 4 | 5 | #menu { 6 | display: none; 7 | } 8 | 9 | #content { 10 | margin-left: 0px !important; 11 | } 12 | 13 | a:not(.uuidhover):not(.more):not(.host_details) { 14 | pointer-events: none; 15 | cursor: default; 16 | color:black; 17 | } -------------------------------------------------------------------------------- /src/simian/mac/admin/groups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Groups admin handler.""" 18 | 19 | import httplib 20 | import urllib 21 | 22 | from simian.mac import admin 23 | from simian.mac import models 24 | 25 | 26 | class Groups(admin.AdminHandler): 27 | """Handler for /admin/groups.""" 28 | 29 | def get(self): 30 | """GET handler.""" 31 | groups = models.Group.all() 32 | groups = sorted(groups, key=lambda t: unicode.lower(t.key().name())) 33 | d = {'groups': groups, 'can_mod_groups': self.IsAdminUser(), 34 | 'report_type': 'groups'} 35 | self.Render('groups.html', d) 36 | 37 | @admin.AdminHandler.XsrfProtected('groups') 38 | def post(self): 39 | """POST handler.""" 40 | if not self.IsAdminUser(): 41 | self.error(httplib.FORBIDDEN) 42 | return 43 | 44 | group_name = urllib.unquote(self.request.get('group').strip()) 45 | action = self.request.get('action') 46 | if action == 'create': 47 | group = models.Group(key_name=group_name) 48 | users = self.request.get_all('user') 49 | if users: 50 | group.users = users 51 | group.put() 52 | msg = 'Group successfully saved.' 53 | elif action == 'delete': 54 | group_manifest_mods = models.GroupManifestModification.all().filter( 55 | 'group_key_name =', group_name).get() 56 | if group_manifest_mods: 57 | msg = "Group not deleted as it's being used for Manifest Modifications." 58 | else: 59 | group = models.Group.get_by_key_name(group_name) 60 | if group: 61 | group.delete() 62 | else: 63 | self.error(httplib.NOT_FOUND) 64 | return 65 | msg = 'Group successfully deleted.' 66 | elif action == 'change': 67 | users = self.request.get_all('user') 68 | add_group = self.request.get('add') == '1' 69 | group = models.Group.get_by_key_name(group_name) 70 | if not group: 71 | self.error(httplib.NOT_FOUND) 72 | return 73 | 74 | if add_group: 75 | group.users += users 76 | else: 77 | group.users = [u for u in group.users if u not in users] 78 | group.put() 79 | msg = 'Group successfully modified.' 80 | 81 | self.redirect('/admin/groups?msg=%s' % msg) 82 | -------------------------------------------------------------------------------- /src/simian/mac/admin/ip_blacklist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """IP Blacklist admin handler.""" 18 | 19 | import httplib 20 | import re 21 | from simian.mac import admin 22 | from simian.mac import models 23 | from simian.mac.common import util 24 | 25 | IP_REGEX = '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$' 26 | 27 | 28 | class IPBlacklist(admin.AdminHandler): 29 | 30 | def get(self): 31 | """GET handler.""" 32 | if not self.IsAdminUser(): 33 | self.error(httplib.FORBIDDEN) 34 | return 35 | ips = {} 36 | try: 37 | ips = util.Deserialize( 38 | models.KeyValueCache.MemcacheWrappedGet('client_exit_ip_blocks', 39 | 'text_value')) 40 | except util.DeserializeError: 41 | pass 42 | d = {'report_type': 'ip_blacklist', 'title': 'IP Blacklist', 'columns': 2, 43 | 'list': sorted(ips.items()), 'labels': ['IP', 'Comment'], 44 | 'regex': ['/%s/' % IP_REGEX, '/^.{0,60}$/'], 45 | 'infopanel': 'Subnet format required (e.g. 192.168.1.0/24)'} 46 | self.Render('list_edit.html', d) 47 | 48 | @admin.AdminHandler.XsrfProtected('ip_blacklist') 49 | def post(self): 50 | """POST handler.""" 51 | if not self.IsAdminUser(): 52 | self.error(httplib.FORBIDDEN) 53 | return 54 | values = self.request.get_all('item_0', None) 55 | comments = self.request.get_all('item_1', None) 56 | if values and (not comments or len(values) != len(comments)): 57 | self.error(httplib.BAD_REQUEST) 58 | return 59 | is_ip = re.compile(IP_REGEX) 60 | if not all(map(is_ip.match, values)): 61 | self.error(httplib.BAD_REQUEST) 62 | self.response.out.write('Malformed IP') 63 | return 64 | ips = dict(zip(values, comments)) 65 | models.KeyValueCache.MemcacheWrappedSet('client_exit_ip_blocks', 66 | 'text_value', 67 | util.Serialize(ips)) 68 | self.redirect('/admin/ip_blacklist?msg=IPs%20saved') 69 | -------------------------------------------------------------------------------- /src/simian/mac/admin/js/forms.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All Rights Reserved. 2 | 3 | /** 4 | * @fileoverview Form validators and other form tools. 5 | * 6 | */ 7 | 8 | 9 | goog.require('goog.array'); 10 | goog.require('goog.dom'); 11 | goog.require('goog.dom.classes'); 12 | goog.require('goog.events'); 13 | goog.require('goog.i18n.DateTimeFormat'); 14 | goog.require('goog.i18n.DateTimeParse'); 15 | goog.require('goog.ui.InputDatePicker'); 16 | 17 | /** 18 | * Disables/enables all the inputs in a form. 19 | * @param {Element} form The form to enable/disable. 20 | * @param {boolean} enabled Enable or disable. 21 | */ 22 | simian.toggleFormEnabled = function(form, enabled) { 23 | goog.array.forEach(goog.dom.getElementsByTagNameAndClass('input', null, form), 24 | function(input) { 25 | input.disabled = !enabled; 26 | }); 27 | goog.array.forEach( 28 | goog.dom.getElementsByTagNameAndClass('button', null, form), 29 | function(button) { 30 | button.disabled = !enabled; 31 | }); 32 | }; 33 | 34 | 35 | /** 36 | * Disables the submit button if an invalid input exists in the form. 37 | * @param {Element} form The form with possibly invalid inputs and submit button 38 | */ 39 | simian.validateSubmit = function(form) { 40 | form.submit.disabled = 41 | document.getElementsByClassName('invalid-input').length > 0; 42 | }; 43 | goog.exportSymbol('simian.validateSubmit', simian.validateSubmit); 44 | 45 | 46 | /** 47 | * Adds the 'invalid-input' class to a field if it fails the regular expression. 48 | * @param {Element} field The field to validate. 49 | * @param {RegExp} regexp The regular expression to match. 50 | * @param {Function} opt_altValid an alternative validation function. 51 | * @param {Function} opt_callback A callback called after validation. 52 | */ 53 | simian.validateField = function(field, regexp, opt_altValid, opt_callback) { 54 | if (!regexp.test(field.value) || opt_altValid && !opt_altValid()) { 55 | goog.dom.classes.add(field, 'invalid-input'); 56 | } else { 57 | goog.dom.classes.remove(field, 'invalid-input'); 58 | } 59 | if (opt_callback) { 60 | opt_callback(false); 61 | } 62 | if (field.form) { 63 | simian.validateSubmit(field.form); 64 | } 65 | }; 66 | goog.exportSymbol('simian.validateField', simian.validateField); 67 | 68 | 69 | /** 70 | * Makes an input a date-input. 71 | * @param {Element} input The input box to choose a date. 72 | * @param {Function} opt_callback Function called on date change. 73 | */ 74 | simian.makeDateInput = function(input, opt_callback) { 75 | var PATTERN = "yyyy'-'MM'-'dd"; 76 | var formatter = new goog.i18n.DateTimeFormat(PATTERN); 77 | var parser = new goog.i18n.DateTimeParse(PATTERN); 78 | var idp = new goog.ui.InputDatePicker(formatter, parser); 79 | idp.decorate(input); 80 | if (opt_callback) { 81 | goog.events.listen(idp, goog.ui.DatePicker.Events.CHANGE, opt_callback); 82 | } 83 | }; 84 | goog.exportSymbol('simian.makeDateInput', simian.makeDateInput); 85 | -------------------------------------------------------------------------------- /src/simian/mac/admin/js/menu.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All Rights Reserved. 2 | 3 | /** 4 | * @fileoverview Functions for expanding/collapsing the menu and saving the 5 | * state. 6 | * 7 | */ 8 | 9 | 10 | goog.provide('simian.menu'); 11 | 12 | goog.require('goog.dom'); 13 | goog.require('goog.dom.classes'); 14 | goog.require('goog.net.Cookies'); 15 | 16 | 17 | // Global variable indicating the state of the navigation menu. 18 | simian.menu.menuState = 19 | goog.net.cookies.get('menupinned', '1') == '1' ? 'pinned' : 'closed'; 20 | 21 | 22 | /** 23 | * Sets a cookie to save the pinned/closed state of the menu. 24 | * @param {boolean} val True to set the menu state as pinned in the cookie. 25 | */ 26 | simian.menu.setPinnedCookie = function(val) { 27 | goog.net.cookies.remove('menupinned'); 28 | goog.net.cookies.set('menupinned', val ? '1' : '0'); 29 | }; 30 | 31 | 32 | /** 33 | * Updates the body's classes to show/hide/style the menu. 34 | */ 35 | simian.menu.updateMenu = function() { 36 | if (simian.menu.menuState == 'pinned') { 37 | goog.dom.classes.add(goog.dom.getDocument().body, 'menupinned'); 38 | goog.dom.classes.remove(goog.dom.getDocument().body, 'menuopen'); 39 | } else if (simian.menu.menuState == 'open') { 40 | goog.dom.classes.remove(goog.dom.getDocument().body, 'menupinned'); 41 | goog.dom.classes.add(goog.dom.getDocument().body, 'menuopen'); 42 | } else { 43 | goog.dom.classes.remove(goog.dom.getDocument().body, 'menupinned'); 44 | goog.dom.classes.remove(goog.dom.getDocument().body, 'menuopen'); 45 | } 46 | }; 47 | goog.exportSymbol('simian.menu.updateMenu', simian.menu.updateMenu); 48 | 49 | 50 | /** 51 | * Toggles the menu open/close. 52 | * @param {boolean} opt_force If true, forces the menu to be toggled. 53 | */ 54 | simian.menu.toggleMenu = function(opt_force) { 55 | if (simian.menu.menuState == 'closed') { 56 | simian.menu.menuState = 'open'; 57 | } else { 58 | if (opt_force) { 59 | simian.menu.menuState = 'closed'; 60 | simian.menu.setPinnedCookie(false); 61 | } 62 | } 63 | simian.menu.updateMenu(); 64 | }; 65 | goog.exportSymbol('simian.menu.toggleMenu', simian.menu.toggleMenu); 66 | 67 | 68 | /** 69 | * Sets the menu state to pinned and saves cookie. 70 | */ 71 | simian.menu.pinMenu = function() { 72 | simian.menu.menuState = 'pinned'; 73 | simian.menu.setPinnedCookie(true); 74 | simian.menu.updateMenu(); 75 | }; 76 | goog.exportSymbol('simian.menu.pinMenu', simian.menu.pinMenu); 77 | 78 | 79 | /** 80 | * Sets the menu state to closed. Useful for hiding the menu on mouseout, etc. 81 | * @param {MouseEvent} e Event. 82 | */ 83 | simian.menu.hideMenu = function(e) { 84 | if (simian.menu.menuState == 'open') { 85 | simian.menu.menuState = 'closed'; 86 | simian.menu.updateMenu(); 87 | } 88 | }; 89 | goog.exportSymbol('simian.menu.hideMenu', simian.menu.hideMenu); 90 | -------------------------------------------------------------------------------- /src/simian/mac/admin/lock_admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Lock Admin handler.""" 18 | 19 | import httplib 20 | 21 | from simian.mac.common import datastore_locks 22 | from simian.mac import admin 23 | from simian.mac import models 24 | 25 | _PACKAGE = 'package' 26 | 27 | 28 | def _ListAllLockedPackages(): 29 | """List all active locks for packages.""" 30 | # pylint: disable=protected-access 31 | # pylint: disable=g-explicit-bool-comparison 32 | locks = datastore_locks._DatastoreLockEntity.query( 33 | datastore_locks._DatastoreLockEntity.acquired == True).fetch() 34 | # pylint: enable=protected-access 35 | # pylint: enable=g-explicit-bool-comparison 36 | 37 | locked_pkgs = [] 38 | for l in locks: 39 | if l.lock_held and l.key.id().startswith(models.PACKAGE_LOCK_PREFIX): 40 | locked_pkgs.append(l.key.id()[len(models.PACKAGE_LOCK_PREFIX):]) 41 | return locked_pkgs 42 | 43 | 44 | def _ForceReleaseLock(pkg_name): 45 | lock_name = models.PACKAGE_LOCK_PREFIX + pkg_name 46 | e = datastore_locks._DatastoreLockEntity.get_by_id(lock_name) # pylint: disable=protected-access 47 | e.acquired = False 48 | e.put() 49 | 50 | 51 | class LockAdmin(admin.AdminHandler): 52 | """Handler for /admin/lock_admin.""" 53 | 54 | @admin.AdminHandler.XsrfProtected('lock_admin') 55 | def post(self): 56 | """POST handler.""" 57 | if not self.IsAdminUser(): 58 | self.error(httplib.FORBIDDEN) 59 | return 60 | 61 | if self.request.get('lock_type') != _PACKAGE: 62 | self.error(httplib.BAD_REQUEST) 63 | return 64 | 65 | pkg_name = self.request.get('lock_name') 66 | _ForceReleaseLock(pkg_name) 67 | 68 | self.redirect('/admin/lock_admin?msg=Lock deleted successfully.') 69 | 70 | def get(self): 71 | """GET handler.""" 72 | if not self.IsAdminUser(): 73 | self.error(httplib.FORBIDDEN) 74 | return 75 | 76 | locks = [] 77 | for pkg in _ListAllLockedPackages(): 78 | locks.append((_PACKAGE, pkg)) 79 | 80 | values = {'report_type': 'lock_admin', 'locks': locks} 81 | self.Render('lock_admin.html', values) 82 | -------------------------------------------------------------------------------- /src/simian/mac/admin/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Main module with WSGI app and URL mappings for the admin UI.""" 18 | 19 | import webapp2 20 | 21 | from google.appengine.ext import webapp 22 | 23 | from simian import settings 24 | from simian.mac.admin import acl_groups 25 | from simian.mac.admin import applesus 26 | from simian.mac.admin import broken_clients 27 | from simian.mac.admin import config 28 | from simian.mac.admin import groups 29 | from simian.mac.admin import host 30 | from simian.mac.admin import ip_blacklist 31 | from simian.mac.admin import lock_admin 32 | from simian.mac.admin import manifest_modifications 33 | from simian.mac.admin import misc 34 | from simian.mac.admin import package 35 | from simian.mac.admin import package_alias 36 | from simian.mac.admin import packages 37 | from simian.mac.admin import panic 38 | from simian.mac.admin import release_report 39 | from simian.mac.admin import summary 40 | from simian.mac.admin import tags 41 | from simian.mac.admin import upload_icon 42 | from simian.mac.admin import uploadpkg 43 | 44 | 45 | webapp.template.register_template_library( 46 | 'simian.mac.admin.custom_filters') 47 | 48 | 49 | app = webapp2.WSGIApplication([ 50 | (r'/admin/?$', summary.Summary), 51 | 52 | (r'/admin/acl_groups/?$', acl_groups.ACLGroups), 53 | (r'/admin/acl_groups/([\w\-\_\.\s\%]+)/?$', acl_groups.ACLGroups), 54 | 55 | (r'/admin/applesus/?$', applesus.AppleSUSAdmin), 56 | (r'/admin/applesus/([\w\-]+)/?$', applesus.AppleSUSAdmin), 57 | (r'/admin/applesus/([\w\-]+)/([\d\w\-]+)$', applesus.AppleSUSAdmin), 58 | 59 | (r'/admin/brokenclients/?$', broken_clients.BrokenClients), 60 | 61 | (r'/admin/config/?$', config.Config), 62 | 63 | (r'/admin/host/([\w\-\_\.\=\|\%, ]+)/?$', host.Host), 64 | 65 | (r'/admin/ip_blacklist/?$', ip_blacklist.IPBlacklist), 66 | 67 | (r'/admin/lock_admin/?$', lock_admin.LockAdmin), 68 | 69 | (r'/admin/release_report/?$', release_report.ReleaseReport), 70 | 71 | (r'/admin/manifest_modifications/?$', 72 | manifest_modifications.ManifestModifications), 73 | 74 | (r'/admin/package/?$', package.Package), 75 | (r'/admin/package/([\w\-\_\.\s\%\+]+)/?$', package.Package), 76 | 77 | (r'/admin/package_alias/?$', package_alias.PackageAlias), 78 | 79 | (r'/admin/packages/?$', packages.Packages), 80 | (r'/admin/packages/([\w\-]+)/?', packages.Packages), 81 | 82 | (r'/admin/panic/?$', panic.AdminPanic), 83 | 84 | (r'/admin/proposals/?$', packages.PackageProposals), 85 | (r'/admin/proposals/([\w\-]+)/?', packages.PackageProposals), 86 | 87 | (r'/admin/tags/?$', tags.Tags), 88 | (r'/admin/groups/?$', groups.Groups), 89 | 90 | (r'/admin/uploadpkg/?$', uploadpkg.UploadPackage), 91 | (r'/admin/upload_icon/([\w\-\_\.\s\%]+)/?$', upload_icon.UploadIcon), 92 | 93 | (r'/admin/([\w\-\_\.\=\|\%]+)$', misc.Misc), 94 | (r'/admin/([\w\-\_\.\=\|\%]+)/([\w\-\_\.\=\|\%]+)$', misc.Misc), 95 | ], debug=settings.DEBUG) 96 | -------------------------------------------------------------------------------- /src/simian/mac/admin/maintenance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Module to help with updating InstallLog schema with new applesus property. 18 | 19 | This file is completely unneeded if you're starting with a clean and empty 20 | InstallLog model in Datastore. 21 | 22 | Previously, models.InstallLog was designed to only hold Munki pkg installs. With 23 | the introduction of Apple SUS integration, Apple SUS and Munki installs were 24 | both stored in InstallLog, but there wasn't a way to tell them apart so an 25 | 'applesus' boolean property was added to InstallLog. 26 | 27 | Furthermore, Simian started keeping track of whether an install was successful 28 | or not in a single "success" boolean, rather than having to check that status 29 | was equal or not equal to 0. 30 | 31 | This module contains two functions that help with the switch from the InstallLog 32 | without the applesus or success properties to the newer InstallLog with the 33 | properties. 34 | 35 | UpdateInstallLogSchema: 36 | call put() on all entities so the applesus and success properties aren't 37 | missing, so reports that filter('applesus', bool) or filter('success', bool) 38 | don't omit such entities. 39 | 40 | This module contains other maintenance functions to assist with schema upgrades 41 | or rebuilding various caches. 42 | """ 43 | 44 | import logging 45 | 46 | from google.appengine.ext import deferred 47 | 48 | from simian.mac import models 49 | from simian.mac.common import gae_util 50 | from simian.mac.cron import reports_cache 51 | 52 | 53 | INSTALL_LOG_MAX_FETCH = 2000 54 | 55 | 56 | def RebuildInstallCounts(): 57 | """Rebuilds "install_counts" dictionary from all InstallLog entities.""" 58 | lock = models.KeyValueCache.get_by_key_name('pkgs_list_cron_lock') 59 | if lock: 60 | lock.delete() 61 | cursor_obj = models.KeyValueCache.get_by_key_name('pkgs_list_cursor') 62 | if cursor_obj: 63 | cursor_obj.delete() 64 | models.ReportsCache.SetInstallCounts({}) 65 | deferred.defer(reports_cache._GenerateInstallCounts) 66 | 67 | 68 | def UpdateInstallLogSchema(cursor=None, num_updated=0): 69 | """Puts all InstallLog entities so any new properties are created.""" 70 | q = models.InstallLog.all() 71 | 72 | if cursor: 73 | logging.debug('Continuing with cursor: %s', cursor) 74 | q.with_cursor(cursor) 75 | 76 | entities = q.fetch(INSTALL_LOG_MAX_FETCH) 77 | if not entities: 78 | logging.debug('No remaining entities to convert.') 79 | return 80 | 81 | entities_to_put = [] 82 | for e in entities: 83 | e.success = e.IsSuccess() 84 | e.applesus = getattr(e, 'applesus', False) 85 | if not getattr(e, 'server_datetime', False): 86 | e.server_datetime = e.mtime 87 | entities_to_put.append(e) 88 | gae_util.BatchDatastoreOp(models.db.put, entities_to_put, 100) 89 | 90 | cursor = q.cursor() 91 | num_updated += len(entities_to_put) 92 | logging.info('%s entities converted', num_updated) 93 | 94 | deferred.defer(UpdateInstallLogSchema, cursor=cursor, num_updated=num_updated) 95 | -------------------------------------------------------------------------------- /src/simian/mac/admin/panic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Panic mode handler.""" 18 | 19 | import httplib 20 | 21 | from google.appengine.api import users 22 | 23 | from simian import settings 24 | from simian.mac import admin 25 | from simian.mac.common import mail 26 | from simian.mac.munki import common 27 | 28 | 29 | class AdminPanic(admin.AdminHandler): 30 | """Handler for /admin/panic.""" 31 | 32 | def get(self): 33 | """GET handler.""" 34 | if not self.IsAdminUser(): 35 | self.error(httplib.FORBIDDEN) 36 | return 37 | 38 | modes = [] 39 | for mode in common.PANIC_MODES: 40 | d = { 41 | 'name': mode, 42 | 'enabled': common.IsPanicMode(mode), 43 | } 44 | modes.append(d) 45 | 46 | self.Render( 47 | 'panic.html', {'modes': modes, 'report_type': 'panic'}) 48 | 49 | @admin.AdminHandler.XsrfProtected('panic') 50 | def post(self): 51 | """POST handler.""" 52 | if not self.IsAdminUser(): 53 | self.error(httplib.FORBIDDEN) 54 | return 55 | 56 | mode = self.request.get('mode') 57 | enabled = self.request.get('enabled') 58 | verify = self.request.get('verify') 59 | 60 | if not verify: 61 | self.Render( 62 | 'panic_set_verify.html', 63 | {'mode': {'name': mode, 'enabled': enabled}, 'report_type': 'panic'}) 64 | else: 65 | if enabled == 'disable': 66 | enabled = False 67 | elif enabled == 'enable': 68 | enabled = True 69 | else: 70 | enabled = None 71 | 72 | if enabled is None: 73 | self.error(httplib.BAD_REQUEST) 74 | else: 75 | try: 76 | common.SetPanicMode(mode, enabled) 77 | 78 | user = users.get_current_user() 79 | subject = 'Panic Mode Update by %s' % user 80 | body = '%s has set \'%s\' for Panic Mode.\n' % (user, enabled) 81 | mail.SendMail(settings.EMAIL_ADMIN_LIST, subject, body) 82 | 83 | self.redirect('/admin/panic') 84 | except ValueError: 85 | self.error(httplib.BAD_REQUEST) 86 | -------------------------------------------------------------------------------- /src/simian/mac/admin/static/bubble_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/bubble_point.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/bubble_point_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/bubble_point_down.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/bubble_point_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/bubble_point_left.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/check_no_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/check_no_box.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/check_no_box_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/check_no_box_white.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/disclosure_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/disclosure_arrow.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/disclosure_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/disclosure_arrow_back.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/disclosure_arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/disclosure_arrow_down.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/dl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/dl.gif -------------------------------------------------------------------------------- /src/simian/mac/admin/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/favicon.ico -------------------------------------------------------------------------------- /src/simian/mac/admin/static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/file.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/info.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/loading_222.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/loading_222.gif -------------------------------------------------------------------------------- /src/simian/mac/admin/static/menutoggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/menutoggle.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/pinyellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/pinyellow.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/restart.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/simian/mac/admin/static/simian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/simian.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/simian_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/simian_header.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/trash_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/trash_white.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/uploadicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/uploadicon.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/uploadicon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/uploadicon_dark.png -------------------------------------------------------------------------------- /src/simian/mac/admin/static/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/simian/fb9c43946ff7ba29be417068d6447cfc0adfe9ef/src/simian/mac/admin/static/x.png -------------------------------------------------------------------------------- /src/simian/mac/admin/tags.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Tags admin handler.""" 18 | 19 | import httplib 20 | import urllib 21 | 22 | from google.appengine.ext import db 23 | 24 | from simian.mac import admin 25 | from simian.mac import models 26 | from simian.mac.common import auth 27 | 28 | 29 | class Tags(admin.AdminHandler): 30 | """Handler for /admin/tags.""" 31 | 32 | def get(self): 33 | """GET handler.""" 34 | can_mod_tags = ( 35 | self.IsAdminUser() or auth.IsSupportUser or auth.IsSecurityUser()) 36 | tags = models.Tag.all() 37 | tags = sorted(tags, key=lambda t: unicode.lower(t.key().name())) 38 | d = {'tags': tags, 'can_mod_tags': can_mod_tags, 39 | 'report_type': 'tags'} 40 | self.Render('tags.html', d) 41 | 42 | @admin.AdminHandler.XsrfProtected('tags') 43 | def post(self): 44 | """POST handler.""" 45 | can_mod_tags = ( 46 | self.IsAdminUser() or auth.IsSupportUser or auth.IsSecurityUser()) 47 | if not can_mod_tags: 48 | return 49 | 50 | tag = self.request.get('tag').strip() 51 | tag = urllib.unquote(tag) 52 | action = self.request.get('action') 53 | if action == 'create': 54 | t = models.Tag(key_name=tag) 55 | uuid = self.request.get('uuid') 56 | if uuid: 57 | key = db.Key.from_path('Computer', uuid) 58 | t.keys.append(key) 59 | t.put() 60 | msg = 'Tag successfully saved.' 61 | elif action == 'delete': 62 | tag_manifest_mods = models.TagManifestModification.all().filter( 63 | 'tag_key_name =', tag).get() 64 | if tag_manifest_mods: 65 | msg = 'Tag not deleted as it\'s being used for Manifest Modifications.' 66 | else: 67 | t = models.Tag.get_by_key_name(tag) 68 | if t: 69 | t.delete() 70 | else: 71 | self.error(httplib.NOT_FOUND) 72 | return 73 | msg = 'Tag successfully deleted.' 74 | elif action == 'change': 75 | uuid = self.request.get('uuid') 76 | add_tag = self.request.get('add') == '1' 77 | t = models.Tag.get_by_key_name(tag) 78 | if not t: 79 | self.error(httplib.NOT_FOUND) 80 | return 81 | 82 | key = db.Key.from_path('Computer', uuid) 83 | if add_tag: 84 | t.keys.append(key) 85 | else: 86 | if key in t.keys: 87 | t.keys.remove(key) 88 | t.put() 89 | msg = 'Tag successfully modified' 90 | 91 | self.redirect('/admin/tags?msg=%s' % msg) 92 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/acl_groups.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}ACL Groups{% endblock %} 4 | 5 | {% block page-content %} 6 |

ACL Groups

7 | {% for g in groups %} 8 |
9 |

{{ g.title }}

- Edit 10 |
    11 | {% for m in g.members %}
  • {{ m }}
  • {% endfor %} 12 |
13 |
14 | {% endfor %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/applesus_installs.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Apple Software Update Installs{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | {% if pkg != "all" %} 8 | {% if failures %} 9 |

Failed Apple SUS Installs: {{ pkg }}

10 | {% else %} 11 |

Successful Apple SUS Installs: {{ pkg }}

12 | {% endif %} 13 | {% endif %} 14 | 15 |
16 | {% include "results_pagination.html" %} 17 |
18 | 19 | {% include "applesus_installs_list.html" %} 20 | 21 | {% include "results_pagination.html" %} 22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/applesus_installs_list.html: -------------------------------------------------------------------------------- 1 | {% if host_report %} 2 |
3 |

Apple Software Updates

4 | ({{ applesus_installs|length }} found, limit {{ limit }}) 5 |
6 | {% endif %} 7 | 8 | {% if applesus_installs %} 9 | 10 | 11 | {% if host_report or pkg == "all" %}{% endif %} 12 | {% if not host_report %} 13 | 14 | {% endif %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for install in applesus_installs %} 22 | 23 | {% if host_report or pkg == "all" %} 24 | 25 | {% endif %} 26 | {% if not host_report %} 27 | 30 | {% endif %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% endfor %} 38 |
PackageUUIDStatusUnattendedOn CorpTimeTime Since
{{ install.package }} 28 | {{ install.uuid|host_uuid_link }} 29 | {{ install.status }}{{ install.unattended|default_if_none:"N/A" }}{{ install.on_corp }}{{ install.mtime }}{{ install.mtime|timesince }}
39 | {% endif %} 40 | 41 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/applesus_log.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Apple Updates Product Logs{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | {% if product_id %} 8 |

Apple Update Logs: {{ product_id }} {{ display_name|default_if_none:"" }}

9 | {% endif %} 10 | 11 |
12 | {% include "results_pagination.html" %} 13 |
14 | 15 | {% if logs %} 16 | 17 | 18 | {% if not product_id %} 19 | 20 | {% endif %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for log in logs %} 28 | 29 | {% if not product_id %} 30 | 32 | {% endif %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 |
Update IDActionTracksUserTimeTime Since
{{ log.product_id }} 31 | {{ log.action }}{{ log.tracks|tracks_display_no_proposals }}{{ log.user|default_if_none:"(auto)" }}{{ log.mtime }}{{ log.mtime|timesince }}
41 | 42 | {% include "results_pagination.html" %} 43 | 44 | {% endif %} 45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/config.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Configuration Settings{% endblock %} 4 | 5 | {% block page-content %} 6 |

PEM Settings

7 |
8 | {% for pem_name, pem in pems.items %} 9 |

{{ pem_name }}

10 |
11 | {% if pem.pem %} 12 | {% if pem.validation == "Valid" %} 13 | Valid 14 | {% else %} 15 | {{ pem.validation }} 16 | {% endif %} 17 | {% else %} 18 | Missing 19 | {% endif %} 20 | {% if not pem.pem or pem.validation != "Valid" %} 21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 | {% endif %} 29 |
30 | {% endfor %} 31 |
32 | 33 |


34 | 35 |

Configuration Settings

36 |
37 | {% for name, setting in settings %} 38 |

{{ setting.title }}

39 |
40 | {% if setting.type == "string" or setting.type == "integer" %} 41 |
42 | 43 | 44 | 49 | 50 |
51 | {% else %} 52 | {# TODO: change to elif when GAE supports Django 1.4 #} 53 | {% if setting.type == "bool" %} 54 |
55 | 56 | 57 | Enable 63 | 64 |
65 | {% else %} 66 | {# TODO: change to elif when GAE supports Django 1.4 #} 67 | {% if setting.type == "random_str" %} 68 | {% if name == "xsrf_secret" %} 69 |
70 | {% else %} 71 | 73 | {% endif %} 74 | 75 | 76 | {{ setting.value }} 77 | 78 |
79 | {% endif %} 80 | {% endif %} 81 | {% endif %} 82 | 83 | {% if setting.comment %} 84 |
{{ setting.comment }}
85 | {% endif %} 86 |
87 | {% endfor %} 88 |
89 | 90 | {% endblock %} 91 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Panic Mode{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | « Back 8 |

9 | 10 |

Error

11 | 12 |

{{ message }}

13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/groups.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Groups{% endblock %} 4 | 5 | {% block page-content %} 6 |
7 | {% if can_mod_groups %} 8 |
9 | Add New Groups 10 | {% if error %}

{{ error }}

{% endif %} 11 |
12 | 13 | 14 | Group: 15 | 16 |
17 |
18 | 19 |

Existing Groups

20 | {% endif %} 21 | 22 | 23 | 24 | 25 | 26 | {% for g in groups %} 27 | 28 | 29 | 30 | 32 | 44 | 45 | {% endfor %} 46 |
GroupsAdminUsersDelete
{{ g.key.name }}{{ g.user }} 31 | {{ g.users|length }} 33 | {% if can_mod_groups %} 34 |
35 | 36 | 37 | 38 | 39 |
40 | {% else %} 41 | N/A 42 | {% endif %} 43 |
47 | 48 |
49 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/host_popup.html: -------------------------------------------------------------------------------- 1 | {% if not computer %} 2 |

No computer with the specified UUID was found.

3 | {% else %} 4 |
5 |
Hostname
6 |
{{ computer.hostname }}, OS X {{ computer.os_version }}
7 |
Owner
8 |
{{ computer.owner }}
9 |
Serial
10 |
{{ computer.serial }}
11 |
Free HDD
12 |
{{ computer.root_disk_free|filesizeformat }}
13 |
Uptime
14 |
{{ computer.uptime|uptime_from_seconds }}
15 |
Remaining packages to install
16 |
{{ computer.pkgs_to_install|length }}
17 |
Last preflight/postflight
18 |
{{ computer.preflight_datetime|timesince }} ago / {{ computer.postflight_datetime|timesince }} ago
19 |
Last MSU Popup
20 |
{{ computer.last_notified_datetime|timesince }} ago
21 | {% if msu_log %} 22 |
Last MSU Log
23 |
{{ msu_log.0.event }}
24 | {% endif %} 25 | {% if tags_list %} 26 |
Tags
27 |
{{ tags_list|join:", " }}
28 | {% endif %} 29 | {% if groups_list %} 30 |
Groups
31 |
{{ groups_list|join:", " }}
32 | {% endif %} 33 |
34 | {% endif %} 35 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/install_problems.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Installation Problems{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 |
8 | {% include "results_pagination.html" %} 9 |
10 | 11 | {% include "install_problems_list.html" %} 12 | 13 | {% include "results_pagination.html" %} 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/install_problems_list.html: -------------------------------------------------------------------------------- 1 | {% if host_report %} 2 |
3 |

Installation Problems

4 | ({{ install_problems|length }} found, limit {{ limit }}) 5 |
6 | {% endif %} 7 | 8 | {% if install_problems %} 9 | 10 | 11 | 12 | {% if not host_report %} 13 | 14 | {% endif %} 15 | 16 | 17 | 18 | {% for problem in install_problems %} 19 | 20 | 21 | {% if not host_report %} 22 | 25 | {% endif %} 26 | 27 | 28 | 29 | {% endfor %} 30 |
DetailsUUIDTimeTime Since
{{ problem.details }} 23 | {{ problem.uuid|host_uuid_link }} 24 | {{ problem.mtime }}{{ problem.mtime|timesince }}
31 | {% endif %} 32 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/installs.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Munki Installs{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | {% if pkg != "all" %} 8 | {% if failures %} 9 |

Failed Installs: {{ pkg }}

10 | {% else %} 11 |

Successful Installs: {{ pkg }}

12 | {% endif %} 13 | {% endif %} 14 | 15 |
16 | {% include "results_pagination.html" %} 17 |
18 | 19 | {% include "installs_list.html" %} 20 | 21 | {% include "results_pagination.html" %} 22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/installs_list.html: -------------------------------------------------------------------------------- 1 | {% if host_report %} 2 |
3 |

Munki Installs

4 | ({{ installs|length }} found, limit {{ limit }}) 5 |
6 | {% endif %} 7 | 8 | {% if installs %} 9 | 10 | 11 | {% if host_report or pkg == "all" %}{% endif %} 12 | {% if not host_report %} 13 | 14 | {% endif %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for install in installs %} 24 | 25 | {% if host_report or pkg == "all" %} 26 | 28 | {% endif %} 29 | {% if not host_report %} 30 | 33 | {% endif %} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% endfor %} 43 |
PackageUUIDStatusUnattendedOn CorpDownload SpeedInstall Duration SecsTimeTime Since
{{ install.package }} 31 | {{ install.uuid|host_uuid_link }} 32 | {{ install.status }}{{ install.unattended|default_if_none:"N/A" }}{{ install.on_corp }}{{ install.dl_kbytes_per_sec|download_speed }}{{ install.duration_seconds|default_if_none:"N/A" }}{{ install.mtime }}{{ install.mtime|timesince }}
44 | {% endif %} 45 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/list_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | 5 | {% block page-content %} 6 | 35 |

{{ title }}

({{ list|length }})
36 | {% if infopanel %}
{{ infopanel }}
{% endif %} 37 |
38 | 39 | 40 | {% if labels %} 41 | {% for label in labels %}{% endfor %} 42 | {% endif %} 43 | 47 |
{{ label }}
48 | + Add 49 |
50 |
51 | 53 |
54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/lock_admin.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Lock Admin{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 |

8 | WARNING: Do *NOT* delete locks unless you understand what you're doing! You should only ever have to do this if uploading a package causes a crash, and further upload attempts fail with "xxxxxx is locked!" errors. 9 |

10 | 11 | {% if not locks %} 12 |

No locks were found.

13 | {% else %} 14 | 15 | 16 | 17 | 18 | {% for l in locks %} 19 | 20 | 21 | 22 | 29 | 30 | {% endfor %} 31 |
Lock TypeLock ValueDelete
{{ l.0 }}{{ l.1 }} 23 |
24 | 25 | 26 | 27 | 28 |
32 | 33 | {% endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/msu_log_summary.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}MSU Logs Summary{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | {% if summaries %} 8 | {% for summary in summaries %} 9 |
10 | {% include "msu_log_summary_list.html" %} 11 |
12 | {% endfor %} 13 | {% else %} 14 | {% if msu_events %} 15 |

MSU Log Event: {{ msu_event_name }}

16 | Back to admin home 17 | 18 |
19 | {% include "results_pagination.html" %} 20 |
21 | 22 | 23 | 24 | {% for e in msu_events %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 |
UUIDUserEvent TimeDescription
{{ e.uuid|host_uuid_link }}{{ e.user }}{{ e.mtime }} ({{ e.mtime|timesince }}){{ e.desc }}
33 | 34 | {% include "results_pagination.html" %} 35 | 36 | {% else %} 37 |

No MSU Log Summary data was found.

38 | {% endif %} 39 | {% endif %} 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/msu_log_summary_list.html: -------------------------------------------------------------------------------- 1 |

Generated {{ summary.mtime|timesince }} ago

2 | 3 | 4 | 5 | {% for val in summary.values %} 6 | 7 | 8 | 9 | 10 | {% endfor %} 11 |
MSU logs since {{ summary.since }}
{{ val.var }}{{ val.val }}
12 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/package_alias.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Package Alias{% endblock %} 4 | 5 | {% block page-content %} 6 |
7 |

Package Alias Admin

8 | 9 | {% if is_admin %} 10 |
11 | Add New or Edit Existing Package Alias 12 |
13 | To silence "Unknown pkg_alias requested" API warnings, 14 | add an alias with a blank Munki package name. 15 |
16 | {% if error %}

{{ error }}

{% endif %} 17 | 18 | 19 | 20 | Package Alias: 21 | Munki Package Name: 22 | 28 | 29 | 30 |
31 | 32 |

Existing Package Aliases

33 | {% endif %} 34 | 35 | 36 | 37 | 38 | 39 | {% for a in package_aliases %} 40 | 41 | 42 | 43 | 48 | 49 | {% endfor %} 50 |
Package AliasMunki Package NameEnabled
{{ a.key.name }}{% if a.munki_pkg_name %}{{ a.munki_pkg_name }}{% endif %} 44 | 46 | 47 |
51 | 52 |
53 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/package_logs.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Logs{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | {% if filename %} 8 |

9 | {% if report_type == "proposal_logs" %} 10 | Proposals 11 | {% else %} 12 | Package 13 | {% endif %} 14 | Logs: {{ filename }}

15 | {% if is_admin %} 16 | (edit) 17 | {% endif %} 18 | {% endif %} 19 | 20 |
21 | {% include "results_pagination.html" %} 22 |
23 | 24 | {% if logs %} 25 | 26 | 27 | 28 | 29 | 30 | {% if not filename %} 31 | 32 | {% endif %} 33 | {% if is_admin %} 34 | 35 | {% endif %} 36 | 37 | 38 | 39 | {% if report_type == "proposal_logs" %} 40 | 41 | 42 | {% else %} 43 | 44 | {% endif %} 45 | 46 | 47 | 48 | {% for log in logs %} 49 | 50 | 53 | {% if log.data.plist and log.data.action == "pkginfo" %} 54 | 58 | 61 | {% else %} 62 | 63 | {% endif %} 64 | {% if not filename %} 65 | 68 | {% endif %} 69 | {% if is_admin %} 70 | 73 | {% endif %} 74 | 75 | 76 | 77 | 78 | {% if report_type == "proposal_logs" %} 79 | 82 | 83 | 84 | {% if log.data.plist and log.data.action == "pkginfo" %} 85 | 86 | 87 | 98 | 99 | {% endif %} 100 | {% endfor %} 101 |
ActionPlistDiffFilenameEdit PackageInstall TypesCatalogsManifestsProposerApproverUserTimeTime Since
51 | {{ log.data.action }} 52 | 55 | 56 | View plist 57 | 59 | View diff 60 |    66 | {{ log.data.filename }} 67 | 71 | (edit) 72 | {{ log.data.install_types|munki_properties }}{{ log.catalogs|tracks_display }}{{ log.manifests|tracks_display }}{{ log.data.user }}{{ log.data.approver}} 80 | {% endif %} 81 | {{ log.data.mtime }}{{ log.data.mtime|timesince }}
  88 | 89 | {% for line in log.data.plist_diff %} 90 | {% if line.start_omitting %} 91 |    ⋮     92 | {% endif %} 93 | {% if line.end_omitting %}{% endif %} 94 | {{ line.line|spacify }}
95 | {% endfor %} 96 |
97 |
102 | 105 | 106 | {% include "results_pagination.html" %} 107 | 108 | {% endif %} 109 | 110 | {% endblock %} 111 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/panic.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Panic Mode{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 |

Panic Mode Interface

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for mode in modes %} 16 | 17 | 18 | 19 | 30 | 31 | {% endfor %} 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/panic_set_verify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simian Panic 6 | 7 | 8 |

Panic Modes interface

9 |

10 | You are about to {{ mode.enabled }} panic mode {{ mode.name }}. 11 | Please confirm... 12 |

13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/plist.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ track }} manifest{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | « Back 8 | View raw XML

9 | 10 |

{{ title|safe }}



11 | 12 | {{ xml|safe }} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/preflight_exits.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Preflight Exits{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 |
8 | {% include "results_pagination.html" %} 9 |
10 | 11 | {% include "preflight_exits_list.html" %} 12 | 13 | {% include "results_pagination.html" %} 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/preflight_exits_list.html: -------------------------------------------------------------------------------- 1 | {% if host_report %} 2 |
3 |

Preflight Exits

4 | ({{ preflight_exits|length }} found, limit {{ limit }}) 5 |
6 | {% endif %} 7 | 8 | {% if preflight_exits %} 9 |
ModeEnabledAction
{{ mode.name }}{{ mode.enabled }} 20 |
21 | 22 | 23 | {% if mode.enabled %} 24 | 25 | {% else %} 26 | 27 | {% endif %} 28 |
29 |
10 | 11 | 12 | {% if not host_report %}{% endif %} 13 | 14 | 15 | 16 | {% for exit in preflight_exits %} 17 | 18 | 19 | {% if not host_report %} 20 | 21 | {% endif %} 22 | 23 | 24 | 25 | {% endfor %} 26 |
Exit ReasonUUIDTimeTime Since
{{ exit.exit_reason }}{{ exit.uuid|host_uuid_link }}{{ exit.mtime }}{{ exit.mtime|timesince }}
27 | {% endif %} 28 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/release_report.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | 5 | {% block head-content %} 6 | 9 | {% endblock %} 10 | 11 | {% block page-content %} 12 | 13 |

Subject:

14 |

{{ title }} {{ report_date }} 15 | {% if contains_forced_install %} 16 | {{ subject_line_flag }} 17 | {% endif %} 18 |

19 |

Body:

20 |

{{ salutation }}

21 |

{{ introduction }}

22 | {% if contains_forced_install %} 23 |

{{ introduction_warning }}

24 | {% endif %} 25 | {% for item in report_items %} 26 | {% spaceless %} 27 |

{{ item.package_name }} {{ item.version }} 28 | {% if item.managed_install %} 29 | {{ managed_install_text }}{{ item.osx_version_string }} 30 | {% else %} 31 | {% if item.optional_install and item.managed_update %} 32 | {{ managed_update_and_optional_text }}{{ item.osx_version_string }} 33 | {% else %} 34 | {% if item.optional_install %} 35 | {{ optional_install_text }}{{ item.osx_version_string }} 36 | {% else %} 37 | {% if item.managed_update %} 38 | {{ managed_update_text }}{{ item.osx_version_string }} 39 | {% else %} 40 | PLEASE SPECIFY INSTALL SCENARIO. 41 | {% endif %} 42 | {% endif %} 43 | {% endif %} 44 | {% endif %} 45 | {% if item.is_forced_install and item.is_unattended %} 46 | {{ unattended_and_forced_text }} {{ item.forced_on_date }}. 47 | {% else %} 48 | {% if item.is_forced_install %} 49 | {{ forced_text }} {{ item.forced_on_date }}. 50 | {% endif %} 51 | {% endif %} 52 | {% if item.restart_required %} 53 | {{ restart_required_text }} 54 | {% endif %} 55 |

56 | {% endspaceless %} 57 | {% endfor %} 58 |

{{ signature }}

59 |

{{ admin_username }}

60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/results_pagination.html: -------------------------------------------------------------------------------- 1 |
2 | {% if results_count != limit %} 3 | {% if results_count %} 4 | Displaying all {{ results_count }} results. 5 | {% endif %} 6 | {% else %} 7 | Displaying {{ results_count }} of max 8 |
10 | {% for key, value in request_query_params.items %} 11 | {% if key != "limit" %} 12 | 13 | {% endif %} 14 | {% endfor %} 15 | 24 |
25 | results. 26 | 27 | {% if next_page_link %} 28 | 29 | Next {{ limit }} » 30 | 31 | {% endif %} 32 | {% endif %} 33 |
34 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/tags.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Tags{% endblock %} 4 | 5 | {% block page-content %} 6 |
7 | {% if can_mod_tags %} 8 |
9 | Add New Tags 10 | {% if error %}

{{ error }}

{% endif %} 11 |
12 | 13 | 14 | Tag: 15 | 16 |
17 |
18 | 19 |

Existing Tags

20 | {% endif %} 21 | 22 | 23 | 24 | 25 | 26 | {% for t in tags %} 27 | 28 | 29 | 30 | 32 | 44 | 45 | {% endfor %} 46 |
TagsAdminHostsDelete
{{ t.key.name }}{{ t.user }}{{ t.keys|length }} 33 | {% if can_mod_tags %} 34 |
35 | 36 | 37 | 38 | 39 |
40 | {% else %} 41 | N/A 42 | {% endif %} 43 |
47 | 48 |
49 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/upload_pkg_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Upload Package{% endblock %} 4 | 5 | {% block page-content %} 6 | 9 |
10 |
11 |

Upload disk image: {{ filename }}



12 |

13 | 14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /src/simian/mac/admin/templates/user_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}User Settings{% endblock %} 4 | 5 | {% block page-content %} 6 | 7 | {% if computers %} 8 | {{ computers|length }} computers with UserSettings found. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for c in computers %} 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 |
HostnameUUIDOwnerTrackConfig TrackClient VersionOS VersionSiteLast PreflightLast PostflightUser Settings
{{ c.hostname }}{{ c.uuid|host_uuid_link }} 21 | {{ c.owner }} 23 | {{ c.track }}{{ c.config_track }}{{ c.client_version }}{{ c.os_version }}{{ c.site }}{{ c.preflight_datetime|timesince }}{{ c.postflight_datetime|timesince }}{{ c.user_settings }}
34 | {% else %} 35 |

There are no clients with UserSettings.

36 | {% endif %} 37 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /src/simian/mac/admin/upload_icon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Module to handle /admin/uploadpkg.""" 18 | 19 | import base64 20 | import hashlib 21 | import httplib 22 | 23 | from simian.mac.common import datastore_locks 24 | import cloudstorage as gcs 25 | from simian import settings 26 | from simian.mac import admin 27 | from simian.mac import models 28 | from simian.mac.common import auth 29 | 30 | 31 | def GetIconGCSPath(pkg): 32 | """Get the icon for package.""" 33 | try: 34 | bucket = settings.ICONS_GCS_BUCKET 35 | except AttributeError: 36 | return '' 37 | if not pkg.name: 38 | return '' 39 | 40 | icon_path = '/%s/%s.png' % ( 41 | bucket, base64.urlsafe_b64encode(pkg.name)) 42 | try: 43 | with gcs.open(icon_path, 'r'): 44 | return icon_path 45 | except gcs.NotFoundError: 46 | return '' 47 | 48 | 49 | class UploadIcon(admin.AdminHandler): 50 | """Handler for /admin/upload_icon.""" 51 | 52 | def _RenderError(self, status_code, msg): 53 | self.Render('error.html', {'message': msg}) 54 | self.response.set_status(status_code) 55 | 56 | @admin.AdminHandler.XsrfProtected('package') 57 | def post(self, filename): 58 | auth.DoUserAuth() 59 | 60 | try: 61 | bucket = settings.ICONS_GCS_BUCKET 62 | except AttributeError: 63 | self._RenderError( 64 | httplib.NOT_FOUND, 'Dedicated icons GCS bucket is not set.') 65 | return 66 | 67 | p = models.PackageInfo.get_by_key_name(filename) 68 | if not p: 69 | self._RenderError( 70 | httplib.NOT_FOUND, 'PackageInfo not found: %s' % filename) 71 | return 72 | if not p.name: 73 | self._RenderError( 74 | httplib.NOT_FOUND, 'PackageInfo Name is empty: %s' % filename) 75 | return 76 | 77 | icon_filename = self.request.POST['icon'].filename 78 | if not icon_filename.endswith('.png'): 79 | self._RenderError( 80 | httplib.BAD_REQUEST, 'Only png icons supported.') 81 | return 82 | 83 | lock = models.GetLockForPackage(p.filename) 84 | try: 85 | lock.Acquire() 86 | except datastore_locks.AcquireLockError: 87 | self._RenderError(httplib.CONFLICT, 'PackageInfo is locked') 88 | return 89 | 90 | content = self.request.POST['icon'].file.read() 91 | 92 | icon_path = '/%s/%s.png' % ( 93 | bucket, base64.urlsafe_b64encode(p.name)) 94 | with gcs.open(icon_path, 'w') as f: 95 | f.write(content) 96 | 97 | plist = p.plist 98 | plist['icon_hash'] = hashlib.sha256(content).hexdigest() 99 | 100 | # replace p._plist 101 | p.plist = str(plist) 102 | 103 | p.put() 104 | 105 | lock.Release() 106 | 107 | for catalog in p.catalogs: 108 | models.Catalog.Generate(catalog) 109 | 110 | self.redirect('/admin/package/%s' % filename) 111 | -------------------------------------------------------------------------------- /src/simian/mac/admin/xsrf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """XSRF generator/validator.""" 18 | 19 | import base64 20 | import hmac 21 | import os 22 | import time 23 | 24 | from google.appengine.api import users 25 | 26 | from simian import settings 27 | 28 | XSRF_DELIMITER = '|#|' 29 | XSRF_VALID_TIME = 3600 # Seconds = 60 minutes 30 | 31 | 32 | def XsrfTokenGenerate(action, user=None, timestamp=None): 33 | """Generate an XSRF token.""" 34 | if not user: 35 | user = users.get_current_user().email() 36 | if not timestamp: 37 | timestamp = time.time() 38 | timestr = str(timestamp) 39 | try: 40 | secret = settings.XSRF_SECRET 41 | except AttributeError: 42 | secret = os.urandom(16).encode('base64')[:20] 43 | settings.XSRF_SECRET = secret 44 | secret = str(secret) # hmac secrets cannot be unicode. 45 | h = hmac.new(secret, XSRF_DELIMITER.join([user, action, timestr])) 46 | return base64.urlsafe_b64encode( 47 | ''.join([h.digest(), XSRF_DELIMITER, timestr])) 48 | 49 | 50 | def XsrfTokenValidate(token, action, user=None, timestamp=None, time_=time): 51 | """Validate an XSRF token.""" 52 | if not token: 53 | return False 54 | if not user: 55 | user = users.get_current_user().email() 56 | if not timestamp: 57 | try: 58 | _, timestr = base64.urlsafe_b64decode(str(token)).rsplit( 59 | XSRF_DELIMITER, 1) 60 | timestamp = float(timestr) 61 | except (ValueError, TypeError): 62 | return False 63 | 64 | if timestamp + XSRF_VALID_TIME < time_.time(): 65 | return False 66 | if token != XsrfTokenGenerate(action, user, timestamp): 67 | return False 68 | return True 69 | -------------------------------------------------------------------------------- /src/simian/mac/api/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/mac/api/groups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Groups API URL handlers.""" 18 | 19 | import httplib 20 | import json 21 | import logging 22 | 23 | 24 | import webapp2 25 | 26 | from simian import settings 27 | from simian.mac import models 28 | from simian.mac.common import auth 29 | 30 | API_INFO_KEY = settings.API_INFO_KEY 31 | 32 | 33 | class GroupHandler(webapp2.RequestHandler): 34 | """Handler for /api/groups/.""" 35 | 36 | def __init__(self, *args, **kwargs): 37 | self.user = None 38 | super(GroupHandler, self).__init__(*args, **kwargs) 39 | 40 | def _DoAuth(self): 41 | try: 42 | self.user = auth.DoOAuthAuth() 43 | except auth.NotAuthenticated: 44 | enable_admin_check = True 45 | # OAuth was either not used or failed, so perform regular user auth. 46 | self.user = auth.DoUserAuth(is_admin=enable_admin_check) 47 | 48 | def _CheckApiKey(self): 49 | key = self.request.headers.get('X-Simian-API-Info-Key') 50 | 51 | if not API_INFO_KEY: 52 | logging.warning('API_INFO_KEY is unset; blocking all API info requests.') 53 | self.response.abort(httplib.UNAUTHORIZED) 54 | elif key != API_INFO_KEY: 55 | self.response.abort(httplib.UNAUTHORIZED) 56 | 57 | def get(self): 58 | """List groups, or list a group's memberhsip. 59 | 60 | Send optional 'group' parameter to list membership of a specified group. 61 | Otherwise a list of groups is returned. 62 | """ 63 | self._CheckApiKey() 64 | 65 | self.response.headers['Content-Type'] = 'application/json' 66 | 67 | group = self.request.get('group') 68 | if group: 69 | existing_group = models.Group.get_by_key_name(group) 70 | if existing_group: 71 | self.response.out.write(json.dumps(existing_group.users)) 72 | else: 73 | self.response.out.write(json.dumps(models.Group.GetAllGroupNames())) 74 | 75 | def post(self): 76 | """Create a new group, or overwrites an existing group's membership list.""" 77 | self._DoAuth() 78 | self._CheckApiKey() 79 | 80 | group = self.request.get('group') 81 | members = list(set(self.request.get('members').split(','))) 82 | 83 | new_group = models.Group(key_name=group, users=members) 84 | new_group.put() 85 | 86 | def put(self): 87 | """Creates a new group, or extends existing group's membership.""" 88 | self._DoAuth() 89 | self._CheckApiKey() 90 | 91 | group = self.request.get('group') 92 | members = self.request.get('members').split(',') 93 | 94 | existing_group = models.Group.get_by_key_name(group) 95 | if existing_group: 96 | existing_group.users.extend(members) 97 | existing_group.users = list(set(existing_group.users)) 98 | existing_group.put() 99 | else: 100 | self.post() 101 | 102 | def delete(self, group=None): 103 | """Delete an entire group.""" 104 | self._DoAuth() 105 | self._CheckApiKey() 106 | 107 | if group: 108 | existing_group = models.Group.get_by_key_name(group) 109 | if existing_group: 110 | existing_group.delete() 111 | -------------------------------------------------------------------------------- /src/simian/mac/api/packages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """API handler for package info.""" 18 | 19 | import httplib 20 | import json 21 | import logging 22 | import webapp2 23 | 24 | from simian import settings 25 | from simian.mac import models 26 | 27 | API_INFO_KEY = settings.API_INFO_KEY 28 | 29 | 30 | PKGINFO_PLIST_KEYS_AND_DEFAULTS = ( 31 | ('display_name', None), 32 | ('autoremove', False), 33 | ('forced_install', False), 34 | ('unattended_install', False), 35 | ('unattended_uninstall', False), 36 | ('uninstallable', True), 37 | ('version', None) 38 | ) 39 | 40 | 41 | class PackageInfo(webapp2.RequestHandler): 42 | 43 | def get(self): 44 | key = self.request.get('key') 45 | 46 | if not API_INFO_KEY: 47 | logging.warning('API_INFO_KEY is unset; blocking all API info requests.') 48 | self.response.set_status(httplib.UNAUTHORIZED) 49 | return 50 | elif key != API_INFO_KEY: 51 | self.response.set_status(httplib.UNAUTHORIZED) 52 | return 53 | 54 | output = {} 55 | 56 | for package in models.PackageInfo.all(): 57 | output[package.filename] = { 58 | 'name': package.name, 59 | 'catalogs': package.catalogs, 60 | 'created': package.created.isoformat(), 61 | 'install_types': package.install_types, 62 | 'manifests': package.manifests, 63 | 'munki_name': package.munki_name, 64 | 'mtime': package.mtime.isoformat(), 65 | } 66 | 67 | for key, default in PKGINFO_PLIST_KEYS_AND_DEFAULTS: 68 | output[package.filename][key] = package.plist.get(key, default) 69 | 70 | self.response.headers['Content-Type'] = 'application/json' 71 | self.response.out.write(json.dumps(output)) 72 | -------------------------------------------------------------------------------- /src/simian/mac/api/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Main module for Simian API including wsgi URL mappings.""" 18 | 19 | import webapp2 20 | 21 | from simian import settings 22 | from simian.mac.api import dynamic_manifest 23 | from simian.mac.api import groups 24 | from simian.mac.api import packages 25 | 26 | 27 | class ServeHello(webapp2.RequestHandler): 28 | """Serve hello page.""" 29 | 30 | def get(self): 31 | """Handle GET.""" 32 | self.response.out.write('

You\'ve reached the Simian API!

') 33 | 34 | 35 | app = webapp2.WSGIApplication([ 36 | (r'/api/dynamic_manifest/?', dynamic_manifest.DynamicManifest), 37 | (r'/api/dynamic_manifest/([^/]+)/([^/]+)/?', 38 | dynamic_manifest.DynamicManifest), 39 | (r'/api/dynamic_manifest/([^/]+)/([^/]+)/([^/]+)/?', 40 | dynamic_manifest.DynamicManifest), 41 | (r'/api/groups/?', groups.GroupHandler), 42 | (r'/api/groups/([^/]+)/?', groups.GroupHandler), 43 | (r'/api/packages/?', packages.PackageInfo), 44 | (r'/api/?$', ServeHello), 45 | ], debug=settings.DEBUG) 46 | -------------------------------------------------------------------------------- /src/simian/mac/app.yaml: -------------------------------------------------------------------------------- 1 | application: ###APPID### 2 | version: 1 3 | # To reduce cost lower to F1. Some features might not work. 4 | instance_class: F4 5 | 6 | runtime: python27 7 | threadsafe: yes 8 | api_version: 1 9 | 10 | inbound_services: 11 | - warmup 12 | 13 | automatic_scaling: 14 | max_concurrent_requests: 3 15 | min_pending_latency: 100ms 16 | max_pending_latency: 450ms 17 | min_idle_instances: 0 18 | max_idle_instances: 1 19 | 20 | builtins: 21 | - remote_api: on 22 | - deferred: on 23 | 24 | libraries: 25 | - name: django 26 | version: 1.5 27 | - name: webapp2 28 | version: latest 29 | - name: pycrypto 30 | version: latest 31 | 32 | env_variables: 33 | DJANGO_SETTINGS_MODULE: 'simian.settings' 34 | 35 | 36 | handlers: 37 | 38 | ### Warmup URL 39 | 40 | - url: /_ah/warmup 41 | script: simian.mac.urls.app 42 | login: admin 43 | secure: always 44 | 45 | ### URLs commonly requested by Munki clients 46 | 47 | - url: /icons/.* 48 | script: simian.mac.urls.app 49 | secure: always 50 | 51 | - url: /pkgs/.* 52 | script: simian.mac.urls.app 53 | secure: always 54 | 55 | - url: /pkgsinfo/.* 56 | script: simian.mac.urls.app 57 | secure: always 58 | 59 | - url: /catalogs/.* 60 | script: simian.mac.urls.app 61 | secure: always 62 | 63 | - url: /manifests/.* 64 | script: simian.mac.urls.app 65 | secure: always 66 | 67 | - url: /reports 68 | script: simian.mac.urls.app 69 | secure: always 70 | 71 | ### Uncomment the following lines to enable client customization. 72 | ### -=WARNING=- 73 | ### The .zip files stored here are publicly accessible, widely known resources. 74 | ### Only enable client_resources if you refrain from including confidential 75 | ### details, and are prepared to mitigate abuse. 76 | ### For more, see https://github.com/munki/munki/wiki/Client-Customization 77 | # - url: /client_resources 78 | # static_dir: client_resources 79 | # secure: always 80 | 81 | ### Apple SUS integration, client repair, Munki log uploads, etc. 82 | 83 | - url: /applesus/.* 84 | script: simian.mac.urls.app 85 | secure: always 86 | 87 | - url: /repair 88 | script: simian.mac.urls.app 89 | secure: always 90 | 91 | - url: /uploadfile/.* 92 | script: simian.mac.urls.app 93 | secure: always 94 | 95 | - url: /auth 96 | script: simian.mac.urls.app 97 | secure: always 98 | 99 | - url: /api/.* 100 | script: simian.mac.api.urls.app 101 | secure: always 102 | 103 | ### Cron Job Handler 104 | 105 | - url: /cron/.* 106 | script: simian.mac.cron.main.app 107 | login: admin 108 | secure: always 109 | 110 | ### Static content for admin UI 111 | 112 | - url: /admin/static 113 | static_dir: simian/mac/admin/static 114 | secure: always 115 | login: required 116 | 117 | - url: .*/admin/css 118 | static_dir: simian/mac/admin/css 119 | secure: always 120 | login: required 121 | 122 | - url: .*/admin/js 123 | static_dir: simian/mac/admin/js 124 | secure: always 125 | login: required 126 | 127 | ### Admin UI 128 | 129 | - url: /admin($|/.*) 130 | script: simian.mac.admin.main.app 131 | secure: always 132 | login: required 133 | 134 | ### Catchall handler 135 | 136 | - url: /.* 137 | script: simian.mac.urls.app 138 | secure: always 139 | login: required 140 | -------------------------------------------------------------------------------- /src/simian/mac/client/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/mac/client/report_broken_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Custom script to report broken installs to Simian. 18 | 19 | In case machines have broken Python installs, this script cannot contain any 20 | Python imports requiring ObjC bindings. This means we cannot use 21 | flight_import, munkicommon, etc. 22 | """ 23 | 24 | import optparse 25 | 26 | from simian.mac.client import client 27 | from simian.mac.client import flight_common 28 | 29 | 30 | def main(): 31 | optparser = optparse.OptionParser() 32 | optparser.add_option( 33 | '-r', '--reason', dest='reason', default='Unknown', 34 | help='Reason for brokenness.') 35 | optparser.add_option( 36 | '-d', '--detail-file', dest='detail_file', 37 | help='File with error details.') 38 | options, _ = optparser.parse_args() 39 | 40 | detail_parts = [] 41 | 42 | if options.detail_file: 43 | try: 44 | detail_parts.append( 45 | 'Failure detail:\n%s' % open(options.detail_file, 'r').read()) 46 | except IOError as e: 47 | detail_parts.append( 48 | 'Could not read detail file %r:\n%s' % (options.detail_file, e)) 49 | 50 | 51 | return_code, stdout, stderr = flight_common.Exec( 52 | ['facter', '-p'], timeout=60, waitfor=0.5) 53 | facter_parts = [ 54 | 'Facter Return Code: %s' % return_code, 55 | 'Facter StdOut:\n%s' % stdout, 56 | ] 57 | if stderr: 58 | facter_parts.append('Facter StdErr:\n%s' % stderr) 59 | detail_parts.append('\n\n'.join(facter_parts)) 60 | 61 | details = ('\n\n' + ('*' * 60) + '\n\n').join( 62 | [part.strip() for part in detail_parts]) 63 | params = {'details': details, 'reason': options.reason} 64 | 65 | url = flight_common.GetServerURL() 66 | c = client.SimianAuthClient( 67 | flight_common.GetClientIdentifier('auto')['uuid'], hostname=url) 68 | c.GetAuthToken() 69 | c.PostReport('broken_client', params) 70 | print 'Reported broken client to server.' 71 | 72 | 73 | if __name__ == '__main__': 74 | main() 75 | -------------------------------------------------------------------------------- /src/simian/mac/client/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | 19 | VERSION = 'simian-2.5' 20 | -------------------------------------------------------------------------------- /src/simian/mac/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Common constants/etc for Simian project.""" 3 | 4 | import re 5 | 6 | 7 | # Track name constants. 8 | STABLE = 'stable' 9 | TESTING = 'testing' 10 | UNSTABLE = 'unstable' 11 | TRACKS = [STABLE, TESTING, UNSTABLE] 12 | DEFAULT_TRACK = STABLE 13 | 14 | 15 | # Install type constants. 16 | MANAGED_INSTALLS = 'managed_installs' 17 | MANAGED_UNINSTALLS = 'managed_uninstalls' 18 | MANAGED_UPDATES = 'managed_updates' 19 | OPTIONAL_INSTALLS = 'optional_installs' 20 | INSTALL_TYPES = [ 21 | MANAGED_INSTALLS, MANAGED_UNINSTALLS, MANAGED_UPDATES, OPTIONAL_INSTALLS] 22 | DEFAULT_INSTALL_TYPE = MANAGED_INSTALLS 23 | 24 | # Manifest Modification Group Names 25 | MANIFEST_MOD_ADMIN_GROUP = 'admin' 26 | MANIFEST_MOD_SUPPORT_GROUP = 'support' 27 | MANIFEST_MOD_SECURITY_GROUP = 'security' 28 | MANIFEST_MOD_GROUPS = [ 29 | MANIFEST_MOD_SUPPORT_GROUP, 30 | MANIFEST_MOD_SECURITY_GROUP 31 | ] 32 | 33 | # Munki plist name allowed characters. 34 | PLIST_NAME_ALLOWED_CHAR_REGEX = r'[^\w\-\.]' 35 | 36 | 37 | def IsValidPlistName(name): 38 | """Verifies if a given plist name is valid. 39 | 40 | Args: 41 | name: str plist name. 42 | Returns: 43 | Boolean. True if the plist name is valid, False otherwise. 44 | """ 45 | if not name or re.search(PLIST_NAME_ALLOWED_CHAR_REGEX, name): 46 | return False 47 | return True 48 | 49 | 50 | def SanitizeUUID(uuid): 51 | """Sanitizes a UUID by lowercasing and removing any prepended "CN=" string. 52 | 53 | Args: 54 | uuid: str uuid. 55 | Returns: 56 | str uuid. 57 | """ 58 | uuid = uuid.lower() 59 | if uuid.startswith('cn='): 60 | uuid = uuid[3:] 61 | return uuid 62 | -------------------------------------------------------------------------------- /src/simian/mac/common/gae_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Shared resources for App Engine.""" 18 | 19 | import logging 20 | 21 | from google.appengine.ext import blobstore 22 | from google.appengine.ext import db 23 | 24 | from simian.mac.common import datastore_locks 25 | 26 | 27 | def BatchDatastoreOp(op, entities_or_keys, batch_size=25): 28 | """Performs a batch Datastore operation on a sequence of keys or entities. 29 | 30 | Args: 31 | op: func, Datastore operation to perform, i.e. db.put or db.delete. 32 | entities_or_keys: sequence, db.Key or db.Model instances. 33 | batch_size: int, number of keys or entities to batch per operation. 34 | """ 35 | for i in xrange(0, len(entities_or_keys), batch_size): 36 | op(entities_or_keys[i:i + batch_size]) 37 | 38 | 39 | def SafeBlobDel(blobstore_key): 40 | """Helper method to delete a blob by its key. 41 | 42 | Args: 43 | blobstore_key: str, a blob key 44 | """ 45 | try: 46 | blobstore.delete_async(blobstore_key) 47 | except blobstore.Error, e: 48 | logging.warning(( 49 | 'blobstore.delete(%s) failed: %s. ' 50 | 'this key is now probably orphaned.'), blobstore_key, str(e)) 51 | 52 | 53 | def SafeEntityDel(entity): 54 | """Helper method to delete an entity. 55 | 56 | Args: 57 | entity: App Engine db.Model instance. 58 | """ 59 | try: 60 | entity.delete_async() 61 | except db.Error, e: 62 | logging.warning(( 63 | 'Model.delete(%s) failed: %s. ' 64 | 'this entity is now probably empty.'), entity.key().name(), str(e)) 65 | 66 | 67 | def GetBlobAndDel(blobstore_key): 68 | """Get a blob, delete it and return what was its contents. 69 | 70 | Note: Only for use with SMALL Blobs (under 1024x1024 bytes). 71 | 72 | Args: 73 | blobstore_key: str, a blob key 74 | Returns: 75 | str, the blob data 76 | """ 77 | blob_reader = blobstore.BlobReader(blobstore_key) 78 | blob_str = blob_reader.read(1024 * 1024) # bigger than any pkginfo 79 | blob_reader.close() 80 | SafeBlobDel(blobstore_key) 81 | return blob_str 82 | 83 | 84 | class QueryIterator(object): 85 | """Class to assist with iterating over big App Engine Datastore queries. 86 | 87 | NOTE: this class is not compatible with queries using filters with IN or !=. 88 | """ 89 | 90 | def __init__(self, query, step=1000): 91 | self._query = query 92 | self._step = step 93 | 94 | def __iter__(self): 95 | """Iterate over query results safely avoiding 30s query limitations.""" 96 | while True: 97 | entities = self._query.fetch(self._step) 98 | if not entities: 99 | raise StopIteration 100 | for entity in entities: 101 | yield entity 102 | self._query.with_cursor(self._query.cursor()) 103 | 104 | 105 | def LockExists(name): 106 | """Returns True if a lock with the given str name exists, False otherwise.""" 107 | e = datastore_locks._DatastoreLockEntity.get_by_id(name) # pylint: disable=protected-access 108 | if e: 109 | return e.lock_held 110 | return False 111 | -------------------------------------------------------------------------------- /src/simian/mac/common/ipcalc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | 19 | """IP utility functions.""" 20 | 21 | 22 | 23 | 24 | def IpToInt(ip): 25 | """Return a integer for an IP string. 26 | 27 | Output is in network byte order. 28 | 29 | Args: 30 | ip: str, IP address, like "192.168.0.1" 31 | Returns: 32 | int 33 | Raises: 34 | ValueError: if IPv6 address is supplied. 35 | """ 36 | # TODO(user): ipv6 should be supported in the future. 37 | if ip and ip.find(':') > -1: 38 | raise ValueError('IPv6') 39 | ip_int = 0 40 | a = map(int, ip.split('.')) 41 | for i in xrange(len(a)): 42 | ip_int += (a[i] << ((3-i)*8)) 43 | return ip_int 44 | 45 | 46 | def IpMaskToInts(ip_mask): 47 | """Transform a network/mask string into integers. 48 | 49 | Output is in network byte order. 50 | 51 | Args: 52 | ip_mask: str, IP address, like "192.168.0.0/24" 53 | Returns: 54 | (int ip, int mask) 55 | Raises: 56 | ValueError: if IPv6 address is supplied. 57 | """ 58 | if ip_mask and ip_mask.find(':') > -1: 59 | raise ValueError('IPv6') 60 | (net, mask) = ip_mask.split('/') 61 | mask = int(mask) 62 | mask_int = ((2 ** mask) - 1) << (32 - mask) 63 | return IpToInt(net), mask_int 64 | 65 | 66 | def IpMaskMatch(ip, ip_mask): 67 | """Check if an IP is inside an IP mask. 68 | 69 | Args: 70 | ip: str, like "192.168.0.1" 71 | ip_mask: str, like "192.168.0.0/24" 72 | Returns: 73 | True or False 74 | Raises: 75 | ValueError: if IPv6 address is supplied. 76 | """ 77 | (ip_int_mask, ip_int_mask_bits) = IpMaskToInts(ip_mask) 78 | ip_int = IpToInt(ip) 79 | return (ip_int & ip_int_mask_bits) == ip_int_mask 80 | -------------------------------------------------------------------------------- /src/simian/mac/common/mail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Module for sending e-mails.""" 18 | 19 | import logging 20 | 21 | from google.appengine.api import mail as mail_tool 22 | from google.appengine.api import taskqueue 23 | from google.appengine.ext import deferred 24 | from google.appengine.runtime import apiproxy_errors 25 | # circular dependency. settings module is not used during initialization. 26 | import simian.settings 27 | 28 | 29 | def _SendMailDeferCallback(message): 30 | try: 31 | message.send() 32 | except mail_tool.Error: 33 | logging.exception('Error deferring email.') 34 | 35 | 36 | def SendMail(recipient, subject, body, defer=True): 37 | try: 38 | message = mail_tool.EmailMessage( 39 | to=recipient, sender=simian.settings.EMAIL_SENDER, 40 | subject=subject, body=body) 41 | except (mail_tool.InvalidEmailError, mail_tool.InvalidSenderError): 42 | logging.exception( 43 | 'Error sendinge email; verify email related configurations.') 44 | else: 45 | try: 46 | if defer: 47 | deferred.defer(_SendMailDeferCallback, message) 48 | else: 49 | _SendMailDeferCallback(message) 50 | except (deferred.Error, taskqueue.Error, apiproxy_errors.Error): 51 | logging.exception('Error deferring email.') 52 | -------------------------------------------------------------------------------- /src/simian/mac/common/retry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Stub.""" 18 | 19 | 20 | 21 | def FuzzedExponentialIntervals(initial_delay, attempts): 22 | return [initial_delay] * attempts 23 | -------------------------------------------------------------------------------- /src/simian/mac/cron.yaml: -------------------------------------------------------------------------------- 1 | # This YAML contains App Engine Scheduled Tasks / Cron Service configuration. 2 | # 3 | # Before making modifications, familiarize yourself with the following 4 | # documentation. 5 | # 6 | # Simian crons download Apple Updates, auto-promote packages, generate and cache 7 | # reports for the web UI, maintain the Datastore (i.e. remove cruft), and more. 8 | # 9 | # Default cron job execution frequency is tailored for small to medium sized 10 | # deployments (100s to 1000s of clients). In many cases, increased frequency 11 | # will keep the web UI reports/etc. more up to date. Increased frequency 12 | # may come at a greater cost, such as Datastore operations and potentially 13 | # instance hours if clients aren't otherwise keeping instance running 24/7. 14 | # For medium to large deployments, where clients are keeping instance warm 24/7, 15 | # extra costs are likely to be minimal and unnoticed, thus max frequency is 16 | # suggested. Max and min frequency suggestions are present in parenthesis 17 | # suffixed to each cron description. 18 | # 19 | # For more, see: 20 | # - Simian Docs: https://github.com/google/simian/wiki/Cron-Configuration 21 | # - App Engine Docs: https://cloud.google.com/appengine/docs/python/config/cron 22 | 23 | cron: 24 | - description: Expired Auth Token Cleanup (10m-1h) 25 | url: /cron/maintenance/authsession_cleanup 26 | schedule: every 1 hours 27 | 28 | - description: Inactivate Computer records after X days (1h-24h) 29 | url: /cron/maintenance/mark_computers_inactive 30 | schedule: every 9 hours 31 | 32 | - description: Update Average Install Durations used in pkg descriptions (1h-6h) 33 | url: /cron/maintenance/update_avg_install_durations 34 | schedule: every 6 hours 35 | 36 | - description: PackageInfo and Blobstore integrity verification (1h-24h) 37 | url: /cron/maintenance/verify_packages 38 | schedule: every 12 hours 39 | 40 | - description: Apple Software Update Catalog Auto-Promotion (1h-8h) 41 | url: /cron/applesus/autopromote 42 | schedule: every 3 hours 43 | 44 | - description: Apple Software Update Catalog Sync (6h-24h) 45 | url: /cron/applesus/catalogsync 46 | schedule: every 24 hours 47 | 48 | - description: Stats Summary Cache for main web UI page (30m-24h) 49 | url: /cron/reports_cache/summary 50 | schedule: every 4 hours 51 | 52 | - description: Install Counts Cache for Package Admin UI (15m-3h) 53 | url: /cron/reports_cache/installcounts 54 | schedule: every 1 hours 55 | 56 | - description: Pending Counts Cache for Package Admin UI (15m-3h) 57 | url: /cron/reports_cache/pendingcounts 58 | schedule: every 1 hours 59 | 60 | - description: Trending Installs - 24 hours (2h-6h) 61 | url: /cron/reports_cache/trendinginstalls/24 62 | schedule: every 4 hours 63 | 64 | - description: Trending Installs - 1 hour (15m-1h) 65 | url: /cron/reports_cache/trendinginstalls/1 66 | schedule: every 1 hours 67 | 68 | - description: MSU User Logs Summary - all (15m-1h) 69 | url: /cron/reports_cache/msu_user_summary 70 | schedule: every 24 hours 71 | 72 | - description: MSU User Logs Summary - 1 day (1h-24h) 73 | url: /cron/reports_cache/msu_user_summary/1 74 | schedule: every 2 hours 75 | 76 | - description: MSU User Logs Summary - 7 days (12h-48h) 77 | url: /cron/reports_cache/msu_user_summary/7 78 | schedule: every 24 hours 79 | 80 | -------------------------------------------------------------------------------- /src/simian/mac/cron/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/mac/cron/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Module containing url handler for all Apple SUS related crons. 18 | 19 | Classes: 20 | AppleSUSCatalogSync: syncs SUS catalogs from Apple. 21 | """ 22 | 23 | import webapp2 24 | 25 | from simian import settings 26 | from simian.mac.cron import applesus 27 | from simian.mac.cron import maintenance 28 | from simian.mac.cron import reports_cache 29 | 30 | 31 | app = webapp2.WSGIApplication([ 32 | # Apple SUS 33 | (r'/cron/applesus/catalogsync$', applesus.AppleSUSCatalogSync), 34 | (r'/cron/applesus/autopromote$', applesus.AppleSUSAutoPromote), 35 | 36 | 37 | # Maintenance 38 | ('/cron/maintenance/authsession_cleanup', maintenance.AuthSessionCleanup), 39 | ('/cron/maintenance/mark_computers_inactive', 40 | maintenance.MarkComputersInactive), 41 | ('/cron/maintenance/verify_packages', maintenance.VerifyPackages), 42 | ('/cron/maintenance/update_avg_install_durations', 43 | maintenance.UpdateAverageInstallDurations), 44 | 45 | # Reports Cache 46 | (r'/cron/reports_cache/([a-z_]+)$', reports_cache.ReportsCache), 47 | (r'/cron/reports_cache/([a-z_]+)/(.*)$', reports_cache.ReportsCache), 48 | ], debug=settings.DEBUG) 49 | -------------------------------------------------------------------------------- /src/simian/mac/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | 19 | 20 | 21 | 22 | import logging 23 | import os 24 | import sys 25 | 26 | # catch fatal datastore errors and provide a friendly but minimal message. 27 | CATCH_DATASTORE_FATAL = True 28 | 29 | 30 | def BasicExceptionPrint(exc_type, exc_value, exc_traceback): 31 | """Print a user-friendly message and log an offending exception. 32 | 33 | Args: 34 | exc_type, exc_value, exc_traceback: 35 | per sys.exc_type, exc_value, exc_traceback documentation 36 | """ 37 | print 'Content-Type: text/plain' 38 | print 39 | print 'Simian is currently experiencing problems that may be' 40 | print 'specific to Simian. They also may be related to a general' 41 | print 'outage in the App Engine infrastructure.' 42 | print 43 | print 'Connectivity to the site will be restored ASAP.' 44 | 45 | try: 46 | import logging 47 | import traceback 48 | log_exception = True 49 | except ImportError: 50 | log_exception = False 51 | 52 | if log_exception: 53 | logging.error('Exception occured\nType: %s\nValue: %s\nTraceback:\n%s' % 54 | (exc_type, exc_value, '\n'.join(traceback.format_tb(exc_traceback)))) 55 | print 'This error has been logged.' 56 | else: 57 | print 'This error has not been logged.' 58 | 59 | 60 | def main(): 61 | from google.appengine.api import users 62 | 63 | try: 64 | from simian.mac import urls 65 | except AttributeError, e: 66 | print 'Content-Type: text/html' 67 | print '' 68 | print 'AttributeError: %s' % str(e) 69 | print '

' 70 | print 'Have you configured settings?' 71 | logging.exception('AttributeError') 72 | return 73 | 74 | main_method = urls.main 75 | 76 | if CATCH_DATASTORE_FATAL: 77 | import google.appengine 78 | try: 79 | main_method() 80 | except ( 81 | google.appengine.api.datastore_errors.Timeout, 82 | google.appengine.api.datastore_errors.InternalError, 83 | google.appengine.runtime.apiproxy_errors.CapabilityDisabledError): 84 | BasicExceptionPrint(*sys.exc_info()) 85 | else: 86 | main_method() 87 | 88 | if __name__ == '__main__': 89 | main() 90 | -------------------------------------------------------------------------------- /src/simian/mac/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """App Engine Models for Simian web application.""" 3 | 4 | 5 | # NOTE(user): Beware! Model module top level definitions can collide! 6 | 7 | # TODO(user): eventually, base should not be exposed. 8 | from simian.mac.models.base import * 9 | from simian.mac.models.munki import * 10 | from simian.mac.models.settings import * 11 | -------------------------------------------------------------------------------- /src/simian/mac/models/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Constants used as portions of various model entities.""" 18 | 19 | from simian.mac.munki import plist as plist_lib 20 | 21 | 22 | # Munki catalog plist XML with Apple DTD/etc, and empty array for filling. 23 | CATALOG_PLIST_XML = ( 24 | plist_lib.PLIST_HEAD + '\n%s\n' + plist_lib.PLIST_FOOT) 25 | -------------------------------------------------------------------------------- /src/simian/mac/munki/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/mac/munki/handlers/catalogs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Catalogs URL handlers.""" 18 | import httplib 19 | 20 | from simian.mac import models 21 | from simian.mac.common import auth 22 | from simian.mac.munki import handlers 23 | 24 | 25 | class Catalogs(handlers.AuthenticationHandler): 26 | """Handler for /catalogs/""" 27 | 28 | def get(self, name): 29 | """Catalog get handler. 30 | 31 | Args: 32 | name: string catalog name to get. 33 | 34 | Returns: 35 | A webapp.Response() response. 36 | """ 37 | auth.DoAnyAuth() 38 | 39 | catalog = models.Catalog.MemcacheWrappedGet(name) 40 | if not catalog: 41 | self.response.set_status(httplib.NOT_FOUND) 42 | return 43 | 44 | header_date_str = self.request.headers.get('If-Modified-Since', '') 45 | if not handlers.IsClientResourceExpired(catalog.mtime, header_date_str): 46 | self.response.set_status(httplib.NOT_MODIFIED) 47 | return 48 | 49 | self.response.headers['Last-Modified'] = catalog.mtime.strftime( 50 | handlers.HEADER_DATE_FORMAT) 51 | 52 | self.response.headers['Content-Type'] = 'text/xml; charset=utf-8' 53 | self.response.out.write(catalog.plist_xml) 54 | -------------------------------------------------------------------------------- /src/simian/mac/munki/handlers/icons.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """serve icons from GCS.""" 18 | import base64 19 | import httplib 20 | import logging 21 | 22 | import cloudstorage as gcs 23 | from simian import settings 24 | from simian.mac.common import auth 25 | from simian.mac.munki import handlers 26 | 27 | 28 | class Icons(handlers.AuthenticationHandler): 29 | """Handler for /icons/.""" 30 | 31 | def get(self, name): 32 | auth.DoAnyAuth() 33 | 34 | if name == '_icon_hashes.plist': 35 | # munki 3.1 starts reading hashes from special plist. 36 | # to avoid 404 spam in logs, replying with 200. 37 | return 38 | 39 | try: 40 | bucket = settings.ICONS_GCS_BUCKET 41 | except AttributeError: 42 | logging.warning('Dedicated icons GCS bucket is not set.') 43 | self.abort(httplib.BAD_REQUEST) 44 | 45 | name = name.split('.')[0] 46 | icon_path = '/%s/%s.png' % (bucket, base64.urlsafe_b64encode(name)) 47 | 48 | try: 49 | with gcs.open(icon_path, 'r') as gcs_file: 50 | self.response.headers['Content-Type'] = 'image/png' 51 | self.response.write(gcs_file.read()) 52 | except gcs.NotFoundError: 53 | self.abort(httplib.NOT_FOUND) 54 | -------------------------------------------------------------------------------- /src/simian/mac/munki/handlers/manifests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Manifest URL handlers.""" 18 | 19 | import httplib 20 | import logging 21 | 22 | from simian.mac.common import auth 23 | from simian.mac.munki import common 24 | from simian.mac.munki import handlers 25 | 26 | 27 | class Manifests(handlers.AuthenticationHandler): 28 | """Handler for /manifests/""" 29 | 30 | def get(self, client_id_str=''): 31 | """Manifest get handler. 32 | 33 | Args: 34 | client_id_str: optional str client_id; only needed for user requests. 35 | 36 | Returns: 37 | A webapp.Response() response. 38 | """ 39 | session = auth.DoAnyAuth() 40 | client_id = handlers.GetClientIdForRequest( 41 | self.request, session=session, client_id_str=client_id_str) 42 | 43 | try: 44 | plist_xml = common.GetComputerManifest( 45 | client_id=client_id, packagemap=False) 46 | except common.ManifestNotFoundError, e: 47 | logging.warning('Invalid manifest requested: %s', str(e)) 48 | self.response.set_status(httplib.NOT_FOUND) 49 | return 50 | except common.ManifestDisabledError, e: 51 | logging.info('Disabled manifest requested: %s', str(e)) 52 | self.response.set_status(httplib.SERVICE_UNAVAILABLE) 53 | return 54 | except common.Error, e: 55 | logging.exception( 56 | '%s, client_id_str=%s', str(e.__class__.__name__), client_id_str) 57 | self.response.set_status(httplib.SERVICE_UNAVAILABLE) 58 | return 59 | 60 | self.response.headers['Content-Type'] = 'text/xml; charset=utf-8' 61 | self.response.out.write(plist_xml) 62 | -------------------------------------------------------------------------------- /src/simian/mac/munki/handlers/uauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | 19 | """Module to handle real user logins via GAE SSO""" 20 | 21 | 22 | 23 | import logging 24 | 25 | from google.appengine.api import users 26 | 27 | from simian import auth as auth_init 28 | from simian.auth import base 29 | from simian.auth import gaeserver 30 | from simian.mac.common import auth 31 | from simian.mac.munki import handlers 32 | 33 | class Error(Exception): 34 | """Base error.""" 35 | 36 | 37 | class NotAuthenticated(Error, base.NotAuthenticated): 38 | """Not Authenticated Error.""" 39 | 40 | 41 | class UserAuth(handlers.AuthenticationHandler): 42 | """Handle for user auth which provides Auth1 token.""" 43 | 44 | def get(self): 45 | """Handle GET.""" 46 | 47 | try: 48 | # already munki authenticated? return, nothing to do. 49 | gaeserver.DoMunkiAuth() 50 | #logging.info('Uauth: session is already authenticated') 51 | return 52 | except gaeserver.NotAuthenticated: 53 | pass 54 | 55 | user = users.get_current_user() 56 | if not user: 57 | #logging.error('Uauth: user is not logged in') 58 | raise NotAuthenticated 59 | 60 | email = user.email() 61 | if auth.IsAdminUser(email): 62 | a = gaeserver.AuthSimianServer() 63 | output = a.SessionCreateUserAuthToken(email, level=gaeserver.LEVEL_ADMIN) 64 | elif auth.IsSupportUser(email): 65 | a = gaeserver.AuthSimianServer() 66 | output = a.SessionCreateUserAuthToken(email, level=gaeserver.LEVEL_BASE) 67 | else: 68 | logging.error('Uauth: user %s is not an admin', email) 69 | raise NotAuthenticated 70 | 71 | if output: 72 | #logging.info('Uauth: success, token = %s', output) 73 | self.response.headers['Set-Cookie'] = '%s=%s; secure; httponly;' % ( 74 | auth_init.AUTH_TOKEN_COOKIE, output) 75 | self.response.out.write(auth_init.AUTH_TOKEN_COOKIE) 76 | else: 77 | #logging.info('Uauth: unknown token') 78 | raise NotAuthenticated 79 | 80 | def post(self): 81 | """Handle POST. 82 | 83 | Because the appengine_rpc module, used by simian.client.UAuth class, uses 84 | the POST http method, define this handler which mirrors the functionaly 85 | of the GET method. 86 | """ 87 | return self.get() 88 | -------------------------------------------------------------------------------- /src/simian/mac/queue.yaml: -------------------------------------------------------------------------------- 1 | queue: 2 | - name: default 3 | rate: 10/s 4 | bucket_size: 10 5 | - name: first 6 | rate: 5/s 7 | bucket_size: 5 8 | - name: serial 9 | rate: 5/s 10 | max_concurrent_requests: 1 11 | -------------------------------------------------------------------------------- /src/simian/mac/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Main module for Simian including wsgi URL mappings.""" 18 | 19 | import webapp2 20 | 21 | from simian import settings 22 | from simian.mac.munki.handlers import applesus 23 | from simian.mac.munki.handlers import auth 24 | from simian.mac.munki.handlers import catalogs 25 | from simian.mac.munki.handlers import icons 26 | from simian.mac.munki.handlers import manifests 27 | from simian.mac.munki.handlers import pkgs 28 | from simian.mac.munki.handlers import pkgsinfo 29 | from simian.mac.munki.handlers import reports 30 | from simian.mac.munki.handlers import uploadfile 31 | 32 | 33 | class RedirectToAdmin(webapp2.RequestHandler): 34 | """Redirect the request to the Admin page.""" 35 | 36 | def get(self): 37 | """Handle GET.""" 38 | self.redirect('/admin') 39 | 40 | 41 | app = webapp2.WSGIApplication([ 42 | # GET Apple Software Update Service catalog with header client-id. 43 | (r'/applesus/?$', applesus.AppleSUS), 44 | (r'/applesus/([^/]+)$', applesus.AppleSUS), 45 | (r'/applesus/([^/]+)/([^/]+)?$', applesus.AppleSUS), 46 | # GET munki catalogs. 47 | (r'/catalogs/([\w\-\.]+)$', catalogs.Catalogs), 48 | # GET munki manifests. 49 | (r'/manifests/([\w\-\_\.\=\|\%]+)$', manifests.Manifests), 50 | (r'/icons/([\s\w\-\_\.\=\|\%]+)$', icons.Icons), 51 | # GET munki packages. 52 | (r'/pkgs/([\w\-\. \%]+)$', pkgs.Packages), 53 | (r'/pkgs\-userauth/([\w\-\. \%]+)$', pkgs.Packages), # forces user auth. 54 | # GET list of all munki packages. 55 | (r'/pkgsinfo/?$', pkgsinfo.PackagesInfo), 56 | # GET munki pkginfo, PUT updated pkginfo 57 | (r'/pkgsinfo/([\w\-\_\.\=\|\%]+)$', pkgsinfo.PackagesInfo), 58 | # POST reports from munki. 59 | (r'/reports$', reports.Reports), 60 | # PUT uploadfile from munki. 61 | (r'/uploadfile/([\w\-]+)/([\w\-\.]+)$', uploadfile.UploadFile), 62 | # GET auth logout, POST munki auth. 63 | (r'/auth/?$', auth.Auth), 64 | (r'/repair/?$', pkgs.ClientRepair), 65 | (r'/repair/([\w\-\_\.\=\|\%]+)$', pkgs.ClientRepair), 66 | (r'/_ah/warmup', RedirectToAdmin), 67 | (r'/?$', RedirectToAdmin), 68 | ], debug=settings.DEBUG) 69 | -------------------------------------------------------------------------------- /src/simian/munki/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/munki/postflight: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec /usr/local/munki/simian/bin/python /usr/local/munki/simian_client.py postflight "$@" 3 | -------------------------------------------------------------------------------- /src/simian/munki/preflight: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec /usr/local/munki/simian/bin/python /usr/local/munki/simian_client.py preflight "$@" 3 | -------------------------------------------------------------------------------- /src/simian/munki/report_broken_client: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec /usr/local/munki/simian/bin/python /usr/local/munki/simian_client.py report_broken_client "$@" 3 | -------------------------------------------------------------------------------- /src/simian/munki/simian_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Wrapper for Simian preflight and postflight functionality.""" 18 | 19 | import getopt 20 | import logging 21 | import sys 22 | 23 | 24 | # TODO(user): This import hack is ugly; make it more elegant, not tied to 2.6. 25 | import os 26 | sys.path.append('/usr/local/munki') 27 | PYTHON_VERSION = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 28 | PYTHON_LIB_ROOT = ( 29 | '/System/Library/Frameworks/Python.framework/Versions/%s/Extras/lib/python/' % PYTHON_VERSION) 30 | sys.path.append(PYTHON_LIB_ROOT) 31 | for directory in os.listdir(PYTHON_LIB_ROOT): 32 | sys.path.append(PYTHON_LIB_ROOT + directory) 33 | 34 | from simian.mac.client import postflight 35 | from simian.mac.client import preflight 36 | from simian.mac.client import report_broken_client 37 | from simian.mac.client import version 38 | 39 | 40 | ACTIONS = ['preflight', 'postflight', 'report_broken_client', 'version'] 41 | 42 | 43 | def PrintOptions(): 44 | print 'One of the following actions is required: ' 45 | print ' ', ', '.join(ACTIONS) 46 | 47 | 48 | def main(args): 49 | opts, args = getopt.gnu_getopt(args, '', ['debug', 'server=']) 50 | 51 | action = args[0] if args else None 52 | if action not in ACTIONS: 53 | PrintOptions() 54 | return 1 55 | 56 | logging.getLogger().setLevel(logging.WARNING) 57 | server_url = None 58 | 59 | for option, value in opts: 60 | if option == '--debug': 61 | logging.getLogger().setLevel(logging.DEBUG) 62 | # override logging.exception to print helpful tracebacks. 63 | def NewLoggingException(msg, *args): 64 | logging.debug(msg, exc_info=sys.exc_info(), *args) 65 | logging.exception = NewLoggingException 66 | elif option == '--server': 67 | server_url = value 68 | 69 | # munki passes a "runtype" to preflight/postflight; i.e. auto, manual, etc. 70 | runtype = args[1] if len(args) > 1 else 'custom' 71 | 72 | if action == 'preflight': 73 | preflight.RunPreflight(runtype, server_url=server_url) 74 | elif action == 'postflight': 75 | postflight.RunPostflight(runtype) 76 | elif action == 'report_broken_client': 77 | report_broken_client.main() 78 | elif action == 'version': 79 | print version.VERSION 80 | else: 81 | PrintOptions() 82 | return 1 83 | return 0 84 | 85 | 86 | if __name__ == '__main__': 87 | sys.exit(main(sys.argv[1:])) 88 | -------------------------------------------------------------------------------- /src/simian/munki/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # # 17 | 18 | VERSION = 'simian-2.2.2' 19 | -------------------------------------------------------------------------------- /src/simian/stubs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Stub entry points for Simian programs.""" 18 | 19 | from google.apputils import run_script_module 20 | 21 | 22 | def RunSimianPreflight(): 23 | from simian.mac.client import preflight 24 | return run_script_module.RunScriptModule(preflight) 25 | 26 | 27 | def RunSimianPostflight(): 28 | from simian.mac.client import postflight 29 | return run_script_module.RunScriptModule(postflight) 30 | -------------------------------------------------------------------------------- /src/simian/util/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/simian/util/appid_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | # 19 | 20 | """Generates a Google App Engine appid based on various settings.""" 21 | 22 | 23 | 24 | 25 | import ConfigParser 26 | 27 | 28 | def GenerateAppID(): 29 | """Returns a Google App Engine appid based on subdomain + domain configs.""" 30 | f = open('etc/simian/settings.cfg', 'r') 31 | cp = ConfigParser.ConfigParser() 32 | cp.readfp(f) 33 | f.close() 34 | subdomain = cp.get('settings', 'subdomain') 35 | domain = cp.get('settings', 'domain') 36 | 37 | if domain == 'appspot.com': 38 | return subdomain 39 | else: 40 | return '%s:%s' % (domain, subdomain) 41 | 42 | 43 | def main(): 44 | print GenerateAppID() 45 | 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /src/simian/util/compile_js.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2012 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | # 19 | # Uses Closure Compiler Service API to compile JavaScript file. 20 | # ./compile_js.py ... 21 | 22 | import httplib 23 | import urllib 24 | import re 25 | import sys 26 | 27 | CLOSURE_SERVICE_DOMAIN = 'closure-compiler.appspot.com' 28 | 29 | BASE_URL = 'https://raw.githubusercontent.com/google/simian/master/src/simian/mac/admin/js/' 30 | 31 | JS_FILES = ['main.js', 'forms.js', 'menu.js', 'net.js', 'tags.js', 'groups.js'] 32 | CODE_URLS = [BASE_URL + f for f in JS_FILES] 33 | 34 | output_js_file = sys.argv[1] 35 | 36 | # Param docs: https://developers.google.com/closure/compiler/docs/api-ref 37 | params = [ 38 | ('compilation_level', 'ADVANCED_OPTIMIZATIONS'), 39 | ('output_format', 'text'), 40 | ('output_info', 'compiled_code'), 41 | ('use_closure_library', True), 42 | ] 43 | for url in CODE_URLS: 44 | params.append(('code_url', url)) 45 | params = urllib.urlencode(params) 46 | 47 | # Always use the following value for the Content-type header. 48 | headers = { "Content-type": "application/x-www-form-urlencoded" } 49 | conn = httplib.HTTPSConnection(CLOSURE_SERVICE_DOMAIN) 50 | conn.request('POST', '/compile', params, headers) 51 | response = conn.getresponse() 52 | response_text = response.read() 53 | conn.close() 54 | 55 | if response.status != 200 or response_text.startswith('Error'): 56 | print >>sys.stderr, 'JS compilation failed: %s' % response_text 57 | sys.exit(1) 58 | 59 | f = open(output_js_file, 'w') 60 | f.write(response_text) 61 | f.close() 62 | -------------------------------------------------------------------------------- /src/simian/util/create_gae_bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Creates a Google App Engine deployable bundle of Simian code. 6 | 7 | set -e 8 | 9 | SIMIAN_ROOT=$1 10 | BUNDLE_ROOT=$SIMIAN_ROOT/gae_bundle/ 11 | SIMIAN_REL_PATH=../src/simian/ 12 | 13 | # Create Google App Engine bundle directory. 14 | rm -rf $BUNDLE_ROOT 15 | mkdir -p $BUNDLE_ROOT/simian 16 | touch $BUNDLE_ROOT/__init__.py 17 | touch $BUNDLE_ROOT/simian/__init__.py 18 | 19 | # Symlink simian namespace. 20 | ln -s ../$SIMIAN_REL_PATH/auth $BUNDLE_ROOT/simian/auth 21 | ln -s ../$SIMIAN_REL_PATH/mac $BUNDLE_ROOT/simian/mac 22 | ln -s ../$SIMIAN_REL_PATH/settings.py $BUNDLE_ROOT/simian/settings.py 23 | 24 | # Symlink gae_resources. 25 | ln -s $SIMIAN_ROOT/gae_resources/client_resources $BUNDLE_ROOT/client_resources 26 | 27 | # Symlink necessary files at the root of the bundle. 28 | ln -s $SIMIAN_REL_PATH/mac/app.yaml $BUNDLE_ROOT/app.yaml 29 | ln -s $SIMIAN_REL_PATH/mac/index.yaml $BUNDLE_ROOT/index.yaml 30 | ln -s $SIMIAN_REL_PATH/mac/queue.yaml $BUNDLE_ROOT/queue.yaml 31 | ln -s $SIMIAN_REL_PATH/mac/cron.yaml $BUNDLE_ROOT/cron.yaml 32 | -------------------------------------------------------------------------------- /src/simian/util/link_module.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | 5 | set -e 6 | 7 | GAE_BUNDLE=gae_bundle/ 8 | 9 | function find_module() { 10 | VE/bin/python </dev/null) 21 | if [[ ! -z "${egg}" ]]; then 22 | echo "${egg}" 23 | fi 24 | } 25 | 26 | function find_egg_dir() { 27 | egg=$(find .eggs -type d -name ${module}-*.egg 2>/dev/null) 28 | if [[ ! -z "${egg}" ]]; then 29 | find "${egg}" -type d -maxdepth 1 -mindepth 1 \! -name EGG-INFO 30 | fi 31 | } 32 | 33 | function link_module() { 34 | local module="$1" 35 | 36 | local egg=$(find_egg_file ${module}) 37 | if [[ ! -z "${egg}" ]]; then 38 | unzip -o "${egg}" -d "${GAE_BUNDLE}" > /dev/null 39 | rm -rf "${GAE_BUNDLE}/EGG-INFO" 40 | return 41 | fi 42 | 43 | local egg=$(find_egg_dir ${module}) 44 | if [[ ! -z "${egg}" ]]; then 45 | cp -fR "${egg}" "${GAE_BUNDLE}" 46 | return 47 | fi 48 | 49 | local path=$(find_module ${module}) 50 | if [[ ! -z "${path}" ]]; then 51 | rm -f "${GAE_BUNDLE}/${module}" 52 | ln -s "${path}" "${GAE_BUNDLE}/${module}" 53 | return 54 | fi 55 | 56 | echo "ERROR: path not found for ${module}. symlink creation failure." 57 | exit 1 58 | } 59 | 60 | link_module "$1" 61 | -------------------------------------------------------------------------------- /src/simian/util/simianfacter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | 5 | """Script to output Simian dependent facter-like output of various variables.""" 6 | 7 | import argparse 8 | import json 9 | import re 10 | import subprocess 11 | from simian import settings 12 | 13 | 14 | def Exec(cmd): 15 | """Executes a process and returns exit code, stdout, stderr. 16 | 17 | Args: 18 | cmd: str or sequence, command and optional arguments to execute. 19 | 20 | Returns: 21 | Tuple. (Integer return code, string standard out, string standard error). 22 | """ 23 | if type(cmd) is str: 24 | shell = True 25 | else: 26 | shell = False 27 | try: 28 | p = subprocess.Popen( 29 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell) 30 | stdout, stderr = p.communicate() 31 | return p.returncode, stdout, stderr 32 | except (IOError, OSError), e: 33 | return (99, '', str(e)) 34 | 35 | 36 | def GetSerialNumber(): 37 | """Returns the machine serial number.""" 38 | return_code, stdout, unused_stderr = Exec( 39 | ['system_profiler', 'SPHardwareDataType']) 40 | 41 | serial_number = 'UNKNOWN_SERIAL_NUMBER' 42 | 43 | if return_code != 0: 44 | return serial_number 45 | 46 | serial_number_re = re.compile('Serial number[^:]+:\s+(.*)', re.IGNORECASE) 47 | 48 | for l in stdout.splitlines(): 49 | m = serial_number_re.search(l) 50 | if m: 51 | serial_number = m.group(1) 52 | break 53 | 54 | return serial_number 55 | 56 | 57 | def GetFacterFacts(): 58 | """Returns a dictionary of facter facts.""" 59 | return_code, stdout, unused_stderr = Exec(['facter', '--show-legacy', '-p', '-j']) 60 | 61 | # If execution of facter was successful build the client identifier 62 | if return_code != 0: 63 | return {} 64 | 65 | facts = {} 66 | 67 | try: 68 | json_facts = json.loads(stdout) 69 | for k in json_facts: 70 | facts[k] = json_facts[k] 71 | except ValueError, e: 72 | logging.error('There was a problem scanning facter JSON output.') 73 | logging.error('Error: %s', str(e)) 74 | 75 | return facts 76 | 77 | 78 | SETTINGS_FACTER = { 79 | 'certname': settings.CERTNAME or GetSerialNumber(), 80 | 'primary_user': settings.PRIMARY_USER, 81 | 'sp_local_host_name': settings.HOSTNAME, 82 | 'configtrack': settings.CONFIGTRACK, 83 | 'simiantrack': settings.SIMIANTRACK, 84 | 'site': settings.SITE, 85 | 'applesus': settings.APPLESUS, 86 | } 87 | 88 | 89 | def main(): 90 | parser = argparse.ArgumentParser(description='Display system facts.') 91 | parser.add_argument('-j', dest='json', action='store_true', 92 | help='output JSON') 93 | args = parser.parse_args() 94 | 95 | facts = GetFacterFacts() 96 | # place Simian configs where facter is lacking (may be 100%). 97 | for name in SETTINGS_FACTER: 98 | if name not in facts: 99 | facts[name] = SETTINGS_FACTER[name] 100 | 101 | if args.json: 102 | print json.dumps(facts) 103 | else: 104 | print """certname => %(certname)s 105 | primary_user => %(primary_user)s 106 | sp_local_host_name => %(sp_local_host_name)s 107 | configtrack => %(configtrack)s 108 | simiantrack => %(simiantrack)s 109 | site => %(site)s 110 | applesus => %(applesus)s""" % facts 111 | 112 | 113 | if __name__ == '__main__': 114 | main() 115 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/appenginesdk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Load App Engine package from zip.""" 18 | 19 | import sys 20 | import os 21 | try: 22 | import google.appengine.runtime 23 | except ImportError: 24 | from pkgutil import extend_path as _extend_path 25 | import google 26 | _path = '%s/gae_server.zip' % os.path.dirname(os.path.realpath(__file__)) 27 | google.__path__ = _extend_path(['%s/google' % _path], google.__name__) 28 | import google.appengine 29 | google.appengine.__path__ = _extend_path(['%s/google/appengine' % _path], google.__name__) 30 | 31 | # webapp have different django detection logic for old runtime. 32 | os.environ['APPENGINE_RUNTIME'] = 'python27' 33 | 34 | import google.appengine.runtime 35 | -------------------------------------------------------------------------------- /src/tests/simian/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/simian/auth/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2012 Google Inc. All Rights Reserved. 4 | # 5 | 6 | # Name of the cookie set by server 7 | AUTH_TOKEN_COOKIE = 'Auth1Token' 8 | -------------------------------------------------------------------------------- /src/tests/simian/auth/client_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """client module tests.""" 18 | 19 | import logging 20 | logging.basicConfig(filename='/dev/null') 21 | 22 | 23 | import mock 24 | import stubout 25 | 26 | from google.apputils import app 27 | from google.apputils import basetest 28 | 29 | from tests.simian import test_settings 30 | from simian.auth import client 31 | 32 | 33 | class AuthSessionSimianClientTest(basetest.TestCase): 34 | """Test AuthSessionSimianClient class.""" 35 | 36 | def testBasic(self): 37 | self.assertTrue(issubclass( 38 | client.AuthSessionSimianClient, client.base.Auth1ClientSession)) 39 | 40 | 41 | class AuthSimianClientTest(basetest.TestCase): 42 | """Test AuthSimianClient class.""" 43 | 44 | def setUp(self): 45 | self.apc = client.AuthSimianClient() 46 | 47 | def testGetSessionClass(self): 48 | self.assertTrue( 49 | self.apc.GetSessionClass() is client.AuthSessionSimianClient) 50 | 51 | def testLoadCaParameters(self): 52 | """Test _LoadCaParameters().""" 53 | self.apc.LoadCaParameters(test_settings) 54 | self.assertEqual(self.apc._ca_pem, test_settings.CA_PUBLIC_CERT_PEM) 55 | self.assertEqual( 56 | self.apc._server_cert_pem, test_settings.SERVER_PUBLIC_CERT_PEM) 57 | self.assertEqual( 58 | self.apc._required_issuer, test_settings.REQUIRED_ISSUER) 59 | 60 | @mock.patch.object(client.util, 'GetCaParameters') 61 | def testLoadCaParametersWhenError(self, m): 62 | """Test _LoadCaParameters().""" 63 | m.side_effect = client.util.CaParametersError 64 | self.assertRaises( 65 | client.CaParametersError, self.apc.LoadCaParameters, test_settings) 66 | 67 | m.assert_called_once_with(test_settings, omit_server_private_key=True) 68 | 69 | 70 | def main(unused_argv): 71 | basetest.main() 72 | 73 | 74 | if __name__ == '__main__': 75 | app.run() 76 | -------------------------------------------------------------------------------- /src/tests/simian/client/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/simian/mac/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/acl_groups_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import httplib 18 | import json 19 | 20 | 21 | import mock 22 | import stubout 23 | import webtest 24 | 25 | from google.apputils import app 26 | from google.apputils import basetest 27 | 28 | from simian.mac import admin 29 | from simian.mac import models 30 | from simian.mac.admin import main as gae_main 31 | from simian.mac.common import auth 32 | from tests.simian.mac.common import test 33 | 34 | 35 | class AclGroupsModuleTest(test.AppengineTest): 36 | 37 | def setUp(self): 38 | super(AclGroupsModuleTest, self).setUp() 39 | self.testapp = webtest.TestApp(gae_main.app) 40 | 41 | def testPostWithoutToken(self): 42 | resp = self.testapp.post('/admin/acl_groups', status=httplib.BAD_REQUEST) 43 | self.assertIn('Invalid XSRF token.', resp.body) 44 | 45 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 46 | @mock.patch.object(admin.template, 'render', return_value='html:)') 47 | def testPostWithValidToken(self, render_mock, _): 48 | newuser = 'zerocool@example.com' 49 | self.testapp.get('/admin/acl_groups', status=httplib.OK) 50 | params = test.GetArgFromCallHistory(render_mock, arg_index=1) 51 | 52 | params = { 53 | 'xsrf_token': params['xsrf_token'], 54 | 'item_0': [newuser] 55 | } 56 | self.testapp.post( 57 | '/admin/acl_groups/support_users', params, status=httplib.FOUND) 58 | 59 | users = json.loads( 60 | models.KeyValueCache.MemcacheWrappedGet('support_users', 'text_value')) 61 | self.assertEqual([newuser], users) 62 | 63 | 64 | def main(unused_argv): 65 | basetest.main() 66 | 67 | 68 | if __name__ == '__main__': 69 | app.run() 70 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/base_handler_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import httplib 18 | 19 | 20 | import webapp2 21 | import webtest 22 | 23 | from google.apputils import app 24 | from google.apputils import basetest 25 | 26 | from simian.mac import admin 27 | from simian.mac.admin import xsrf 28 | from tests.simian.mac.common import test 29 | 30 | 31 | class MockHandler(admin.AdminHandler): 32 | """Mock handler.""" 33 | 34 | def get(self): 35 | self.response.write(xsrf.XsrfTokenGenerate('mock')) 36 | 37 | @admin.AdminHandler.XsrfProtected('mock') 38 | def post(self): 39 | self.response.write('Content.') 40 | 41 | 42 | class BaseHandlerTest(test.AppengineTest): 43 | 44 | def setUp(self): 45 | super(BaseHandlerTest, self).setUp() 46 | webapp = webapp2.WSGIApplication([('/', MockHandler)]) 47 | self.testapp = webtest.TestApp(webapp) 48 | 49 | def testClickjackingPrevention(self): 50 | resp = self.testapp.get('/') 51 | self.assertEqual( 52 | 'frame-ancestors \'self\'', 53 | resp.headers[admin.CONTENT_SECURITY_POLICY_HEADER]) 54 | 55 | def testXsrfProtection(self): 56 | resp = self.testapp.get('/', status=httplib.OK) 57 | 58 | self.testapp.post('/', {'xsrf_token': resp.body}, status=httplib.OK) 59 | 60 | 61 | def main(_): 62 | basetest.main() 63 | 64 | 65 | if __name__ == '__main__': 66 | app.run() 67 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/broken_clients_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import datetime 18 | 19 | import mock 20 | import stubout 21 | import webtest 22 | 23 | from google.apputils import basetest 24 | 25 | from simian.mac import admin 26 | from simian.mac import models 27 | from simian.mac.admin import main as gae_main 28 | from simian.mac.admin import xsrf 29 | from simian.mac.common import auth 30 | from tests.simian.mac.common import test 31 | 32 | 33 | def _ComputersListToUuidList(computers): 34 | return [c.uuid for c in computers] 35 | 36 | 37 | class BrokenClientModuleTest(test.AppengineTest): 38 | 39 | def setUp(self): 40 | super(BrokenClientModuleTest, self).setUp() 41 | self.testapp = webtest.TestApp(gae_main.app) 42 | 43 | @mock.patch.object(auth, 'IsGroupMember', return_value=True) 44 | @mock.patch.object(admin.AdminHandler, 'Render') 45 | def testGet(self, render_mock, _): 46 | past = datetime.datetime.now() - datetime.timedelta(days=1) 47 | 48 | broken_uuid = 'uuid1' 49 | models.Computer( 50 | key_name=broken_uuid, preflight_datetime=past, uuid=broken_uuid).put() 51 | models.ComputerClientBroken( 52 | uuid=broken_uuid, details='42343243234', connections_off_corp=50, 53 | broken_datetimes=[datetime.datetime.now()]).put() 54 | 55 | toomany_preflight_uuid = 'toomany_preflight_since_postflight' 56 | models.Computer( 57 | uuid=toomany_preflight_uuid, connections_on_corp=40, 58 | preflight_count_since_postflight=50, postflight_datetime=past, 59 | preflight_datetime=datetime.datetime.now(), 60 | ).put() 61 | 62 | models.Computer( 63 | uuid='not_broken', connections_on_corp=40, 64 | preflight_count_since_postflight=3, postflight_datetime=past, 65 | preflight_datetime=datetime.datetime.now(), 66 | ).put() 67 | 68 | no_connections_uuid = 'client without connections' 69 | models.Computer( 70 | uuid=no_connections_uuid, preflight_count_since_postflight=6).put() 71 | 72 | self.testapp.get('/admin/brokenclients') 73 | 74 | args = test.GetArgFromCallHistory(render_mock, arg_index=1) 75 | 76 | self.assertEqual( 77 | [no_connections_uuid], 78 | _ComputersListToUuidList(args['zero_conn_computers'])) 79 | 80 | self.assertEqual( 81 | [broken_uuid], _ComputersListToUuidList(args['py_computers'])) 82 | 83 | self.assertEqual( 84 | [toomany_preflight_uuid], 85 | _ComputersListToUuidList(args['pf_computers'])) 86 | 87 | @mock.patch.object(auth, 'IsGroupMember', return_value=True) 88 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=True) 89 | def testMarkAsFixed(self, *_): 90 | broken_uuid = 'client_id' 91 | models.ComputerClientBroken( 92 | key_name=broken_uuid, uuid=broken_uuid, details='42343243234', 93 | connections_off_corp=50, broken_datetimes=[datetime.datetime.now()], 94 | fixed=False).put() 95 | 96 | self.testapp.post( 97 | '/admin/brokenclients', {'action': 'set_fixed', 'uuid': broken_uuid}) 98 | 99 | self.assertTrue( 100 | models.ComputerClientBroken.get_by_key_name(broken_uuid).fixed) 101 | 102 | 103 | if __name__ == '__main__': 104 | basetest.main() 105 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/ip_blacklist_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import httplib 18 | 19 | 20 | import mock 21 | import stubout 22 | import webtest 23 | 24 | from google.apputils import basetest 25 | 26 | from simian.mac import admin 27 | from simian.mac import models 28 | from simian.mac.admin import main as gae_main 29 | from simian.mac.admin import xsrf 30 | from simian.mac.common import auth 31 | from tests.simian.mac.common import test 32 | from simian.mac.common import util 33 | 34 | 35 | class IpBlacklistModuleTest(test.AppengineTest): 36 | 37 | def setUp(self): 38 | super(IpBlacklistModuleTest, self).setUp() 39 | self.testapp = webtest.TestApp(gae_main.app) 40 | 41 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 42 | @mock.patch.object(admin.AdminHandler, 'Render') 43 | def testGet(self, render_mock, _): 44 | data = {'192.168.1.1': 'zerocool'} 45 | models.KeyValueCache.MemcacheWrappedSet( 46 | 'client_exit_ip_blocks', 'text_value', util.Serialize(data)) 47 | 48 | self.testapp.get('/admin/ip_blacklist', status=httplib.OK) 49 | 50 | args = test.GetArgFromCallHistory(render_mock, arg_index=1) 51 | self.assertEquals(data.items(), args['list']) 52 | 53 | @mock.patch.object(auth, 'IsAdminUser', return_value=False) 54 | def testGetAccessDenied(self, *_): 55 | self.testapp.get('/admin/ip_blacklist', status=httplib.FORBIDDEN) 56 | 57 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 58 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=True) 59 | def testSet(self, *_): 60 | self.testapp.post( 61 | '/admin/ip_blacklist', {'item_0': '192.168.1.1', 'item_1': 'zerocool'}, 62 | status=httplib.FOUND) 63 | self.assertEqual( 64 | {'192.168.1.1': 'zerocool'}, 65 | util.Deserialize(models.KeyValueCache.MemcacheWrappedGet( 66 | 'client_exit_ip_blocks', 'text_value'))) 67 | 68 | 69 | if __name__ == '__main__': 70 | basetest.main() 71 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/package_alias_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import httplib 18 | 19 | 20 | import mock 21 | import stubout 22 | import webtest 23 | 24 | from google.apputils import basetest 25 | 26 | from simian.mac import admin 27 | from simian.mac import models 28 | from simian.mac.admin import main as gae_main 29 | from simian.mac.admin import xsrf 30 | from simian.mac.common import auth 31 | from tests.simian.mac.common import test 32 | 33 | 34 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=True) 35 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 36 | class PackageAliasModuleTest(test.AppengineTest): 37 | 38 | def setUp(self): 39 | super(PackageAliasModuleTest, self).setUp() 40 | self.testapp = webtest.TestApp(gae_main.app) 41 | 42 | def testCreatePackageAlias(self, *_): 43 | aliasname = 'aliasname' 44 | pkgname = 'pkgname' 45 | models.PackageInfo(name=pkgname).put() 46 | 47 | self.testapp.post( 48 | '/admin/package_alias', 49 | {'create_package_alias': 1, 'package_alias': aliasname, 50 | 'munki_pkg_name': pkgname}, status=httplib.FOUND) 51 | 52 | aliases = models.PackageAlias.all().fetch(None) 53 | self.assertEqual(1, len(aliases)) 54 | self.assertEqual(aliasname, aliases[0].key().name()) 55 | self.assertEqual(pkgname, aliases[0].munki_pkg_name) 56 | self.assertTrue(aliases[0].enabled) 57 | 58 | def testCreatePackageAliasPackageDoesNotExist(self, *_): 59 | aliasname = 'aliasname' 60 | pkgname = 'does_not_exist' 61 | self.testapp.post( 62 | '/admin/package_alias', 63 | {'create_package_alias': 1, 'package_alias': aliasname, 64 | 'munki_pkg_name': pkgname}, status=httplib.FOUND) 65 | 66 | aliases = models.PackageAlias.all().fetch(None) 67 | self.assertEqual(0, len(aliases)) 68 | 69 | def testDisablePackageAlias(self, *_): 70 | aliasname = 'aliasname' 71 | pkgname = 'pkgname' 72 | models.PackageAlias(key_name=aliasname, munki_pkg_name=pkgname).put() 73 | 74 | self.testapp.post( 75 | '/admin/package_alias', 76 | {'enabled': 0, 'key_name': aliasname}, status=httplib.OK) 77 | 78 | aliases = models.PackageAlias.all().fetch(None) 79 | self.assertEqual(1, len(aliases)) 80 | self.assertFalse(aliases[0].enabled) 81 | 82 | @mock.patch.object(admin.AdminHandler, 'Render') 83 | def testDisplayMain(self, render_mock, *_): 84 | aliasname = 'aliasname' 85 | pkgname = 'pkgname' 86 | models.PackageAlias(key_name=aliasname, munki_pkg_name=pkgname).put() 87 | 88 | self.testapp.get('/admin/package_alias') 89 | 90 | args = test.GetArgFromCallHistory(render_mock, arg_index=1) 91 | self.assertEqual(1, len(args['package_aliases'].fetch(None))) 92 | 93 | 94 | if __name__ == '__main__': 95 | basetest.main() 96 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/release_report_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from google.apputils import basetest 19 | 20 | from simian.mac.admin import release_report 21 | 22 | 23 | class ReleaseReportModelTest(basetest.TestCase): 24 | """Test the release_report module.""" 25 | 26 | def testGetOSXMajorVersion(self): 27 | self.assertEqual('5', release_report.GetOSXMajorVersion('10.5')) 28 | self.assertEqual('10', release_report.GetOSXMajorVersion('10.10')) 29 | self.assertEqual('11', release_report.GetOSXMajorVersion('10.11')) 30 | self.assertEqual('11', release_report.GetOSXMajorVersion('10.11.1')) 31 | self.assertEqual('9', release_report.GetOSXMajorVersion('10.9.10')) 32 | self.assertEqual(None, release_report.GetOSXMajorVersion('10')) 33 | self.assertEqual(None, release_report.GetOSXMajorVersion(None)) 34 | 35 | 36 | if __name__ == '__main__': 37 | basetest.main() 38 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/tags_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import httplib 18 | 19 | 20 | import mock 21 | import stubout 22 | import webtest 23 | 24 | from google.apputils import basetest 25 | 26 | from simian.mac import admin 27 | from simian.mac import models 28 | from simian.mac.admin import main as gae_main 29 | from simian.mac.admin import xsrf 30 | from simian.mac.common import auth 31 | from tests.simian.mac.common import test 32 | 33 | 34 | class TagsModuleTest(test.AppengineTest): 35 | 36 | def setUp(self): 37 | super(TagsModuleTest, self).setUp() 38 | self.testapp = webtest.TestApp(gae_main.app) 39 | 40 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 41 | @mock.patch.object(admin.AdminHandler, 'Render') 42 | def testGet(self, render_mock, _): 43 | tagname = 'ak47' 44 | models.Tag(key_name=tagname).put() 45 | 46 | self.testapp.get('/admin/tags', status=httplib.OK) 47 | 48 | args = test.GetArgFromCallHistory(render_mock, arg_index=1) 49 | self.assertEqual(tagname, args['tags'][0].key().name()) 50 | 51 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 52 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=True) 53 | def testCreateTag(self, *_): 54 | tagname = 't1' 55 | self.testapp.post( 56 | '/admin/tags', {'action': 'create', 'tag': tagname, 'uuid': 'id1'}, 57 | status=httplib.FOUND) 58 | 59 | tags = models.Tag.all().fetch(None) 60 | self.assertEqual(1, len(tags)) 61 | self.assertEqual(tags[0].key().name(), tagname) 62 | self.assertEqual(1, len(tags[0].keys)) 63 | 64 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 65 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=True) 66 | def testDeleteTag(self, *_): 67 | tagname = 'ak47' 68 | models.Tag(key_name=tagname).put() 69 | self.testapp.post( 70 | '/admin/tags', {'action': 'delete', 'tag': tagname}, 71 | status=httplib.FOUND) 72 | 73 | tags = models.Tag.all().fetch(None) 74 | self.assertEqual(0, len(tags)) 75 | 76 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 77 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=True) 78 | def testModifyTag(self, *_): 79 | uuid = 'id1' 80 | tagname = 'ak47' 81 | models.Tag(key_name=tagname).put() 82 | self.testapp.post( 83 | '/admin/tags', 84 | {'action': 'change', 'tag': tagname, 'add': 1, 'uuid': uuid}, 85 | status=httplib.FOUND) 86 | 87 | tags = models.Tag.all().fetch(None) 88 | self.assertEqual(1, len(tags)) 89 | self.assertEqual(1, len(tags[0].keys)) 90 | self.assertEqual(uuid, tags[0].keys[0].name()) 91 | 92 | @mock.patch.object(auth, 'IsAdminUser', return_value=True) 93 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=False) 94 | def testInvalidXsrfToken(self, *_): 95 | tagname = 't1' 96 | self.testapp.post( 97 | '/admin/tags', {'action': 'create', 'tag': tagname, 'uuid': 'id1'}, 98 | status=httplib.BAD_REQUEST) 99 | 100 | 101 | if __name__ == '__main__': 102 | basetest.main() 103 | -------------------------------------------------------------------------------- /src/tests/simian/mac/admin/upload_icon_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import httplib 18 | import os 19 | 20 | 21 | import mock 22 | import stubout 23 | import webtest 24 | 25 | from google.apputils import app 26 | from google.apputils import resources 27 | from google.apputils import basetest 28 | 29 | from simian import settings 30 | from simian.mac import models 31 | from simian.mac.admin import main as gae_main 32 | from simian.mac.admin import xsrf 33 | from simian.mac.common import auth 34 | from tests.simian.mac.common import test 35 | 36 | 37 | PLIST_FILE = 'simian/mac/common/testdata/testpackage.plist' 38 | 39 | 40 | def GetTestData(rel_path): 41 | 42 | path = os.path.dirname(os.path.realpath(__file__)) 43 | while os.path.basename(path) != 'tests': 44 | path = os.path.dirname(path) 45 | with open(os.path.join(path, rel_path)) as f: 46 | return f.read() 47 | 48 | 49 | @mock.patch.object(auth, 'IsGroupMember', return_value=True) 50 | @mock.patch.object(xsrf, 'XsrfTokenValidate', return_value=True) 51 | class UploadIconModuleTest(test.AppengineTest): 52 | 53 | def setUp(self): 54 | super(UploadIconModuleTest, self).setUp() 55 | self.testapp = webtest.TestApp(gae_main.app) 56 | 57 | self.plist = GetTestData(PLIST_FILE) 58 | 59 | def testGCSBucketNotSet(self, *_): 60 | resp = self.testapp.post( 61 | '/admin/upload_icon/filename', status=httplib.NOT_FOUND) 62 | self.assertIn('GCS bucket is not set', resp.body) 63 | 64 | def testSuccess(self, *_): 65 | settings.ICONS_GCS_BUCKET = 'test' 66 | 67 | filename = 'testpackage.dmg' 68 | munki_name = 'testpackage' 69 | models.PackageInfo( 70 | key_name=filename, filename=filename, 71 | name=munki_name, _plist=self.plist).put() 72 | content = 'ICON_CONTETN' 73 | resp = self.testapp.post( 74 | '/admin/upload_icon/%s' % filename, 75 | upload_files=[('icon', '1.png', content)], status=httplib.FOUND) 76 | self.assertTrue( 77 | resp.headers['Location'].endswith('/admin/package/%s' % filename)) 78 | 79 | 80 | def main(unused_argv): 81 | basetest.main() 82 | 83 | 84 | if __name__ == '__main__': 85 | app.run() 86 | -------------------------------------------------------------------------------- /src/tests/simian/mac/api/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/simian/mac/api/info_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2016 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """info module tests.""" 18 | 19 | import mox 20 | import stubout 21 | 22 | from google.apputils import app 23 | from google.apputils import basetest 24 | from simian.mac.api import info 25 | 26 | 27 | class InfoModuleTest(mox.MoxTestBase): 28 | 29 | def setUp(self): 30 | mox.MoxTestBase.setUp(self) 31 | self.stubs = stubout.StubOutForTesting() 32 | 33 | def tearDown(self): 34 | self.mox.UnsetStubs() 35 | self.stubs.UnsetAll() 36 | 37 | # TODO(user): Add tests for info.InfoHandler class. 38 | 39 | 40 | def main(unused_argv): 41 | basetest.main() 42 | 43 | 44 | if __name__ == '__main__': 45 | app.run() 46 | -------------------------------------------------------------------------------- /src/tests/simian/mac/api/packages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """API handler for package info.""" 18 | 19 | import json 20 | import logging 21 | import webapp2 22 | 23 | from simian import settings 24 | from simian.mac import models 25 | 26 | API_INFO_KEY = settings.API_INFO_KEY 27 | 28 | 29 | PKGINFO_PLIST_KEYS_AND_DEFAULTS = ( 30 | ('display_name', None), 31 | ('autoremove', False), 32 | ('forced_install', False), 33 | ('unattended_install', False), 34 | ('uninstallable', True), 35 | ('version', None) 36 | ) 37 | 38 | 39 | class PackageInfo(webapp2.RequestHandler): 40 | 41 | def get(self): 42 | key = self.request.get('key') 43 | 44 | if not API_INFO_KEY: 45 | logging.warning('API_INFO_KEY is unset; blocking all API info requests.') 46 | self.response.set_status(401) 47 | return 48 | elif key != API_INFO_KEY: 49 | self.response.set_status(401) 50 | return 51 | 52 | output = {} 53 | 54 | for package in models.PackageInfo.all(): 55 | output[package.filename] = { 56 | 'name': package.name, 57 | 'catalogs': package.catalogs, 58 | 'created': package.created.isoformat(), 59 | 'install_types': package.install_types, 60 | 'manifests': package.manifests, 61 | 'munki_name': package.munki_name, 62 | 'mtime': package.mtime.isoformat(), 63 | } 64 | 65 | for key, default in PKGINFO_PLIST_KEYS_AND_DEFAULTS: 66 | output[package.filename][key] = package.plist.get(key, default) 67 | 68 | self.response.headers['Content-Type'] = 'application/json' 69 | self.response.out.write(json.dumps(output)) 70 | -------------------------------------------------------------------------------- /src/tests/simian/mac/client/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/simian/mac/client/munkicommon_mock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | 19 | import sys 20 | 21 | 22 | class munkicommon(object): # pylint: disable=g-bad-name 23 | """Mock munkicommon module.""" 24 | 25 | ADDITIONAL_HTTP_HEADERS_KEY = 'AdditionalHttpHeaders' 26 | 27 | @classmethod 28 | def ManagedInstallsPreferences(cls): 29 | return {} 30 | 31 | @classmethod 32 | def SecureManagedInstallsPreferences(cls): 33 | return {} 34 | 35 | @classmethod 36 | def cleanUpTmpDir(cls): # pylint: disable=g-bad-name 37 | pass 38 | 39 | 40 | def LoadMockModules(): 41 | sys.modules['munkilib'] = sys.modules[__name__] 42 | -------------------------------------------------------------------------------- /src/tests/simian/mac/client/postflight_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | 19 | import mox 20 | import stubout 21 | 22 | from google.apputils import basetest 23 | 24 | # Import and load mock modules before importing preflight. 25 | # pylint: disable=g-bad-import-order 26 | # pylint: disable=g-import-not-at-top 27 | from tests.simian.mac.client import munkicommon_mock 28 | munkicommon_mock.LoadMockModules() 29 | 30 | from simian.mac.client import postflight 31 | 32 | 33 | class PostflightTest(mox.MoxTestBase): 34 | 35 | def setUp(self): 36 | mox.MoxTestBase.setUp(self) 37 | 38 | def tearDown(self): 39 | self.mox.UnsetStubs() 40 | 41 | def testRunPostflight(self): 42 | pkgs_to_install = ['pkg1', 'pkg2'] 43 | updates_to_install = ['update1', 'update2'] 44 | client_id = { 45 | 'uuid': 'abcd4077-0b34-4572-ba91-cc7aad032b5c', 46 | 'on_corp': '1', 47 | } 48 | expected_report = { 49 | 'apple_updates_to_install': updates_to_install, 50 | 'client_id': 'fake_string', 51 | 'pkgs_to_install': pkgs_to_install, 52 | } 53 | mock_url = 'http://test-url' 54 | mock_client = self.mox.CreateMockAnything() 55 | 56 | self.mox.StubOutWithMock(postflight.flight_common, 'GetServerURL') 57 | self.mox.StubOutWithMock(postflight.flight_common, 'GetAuth1Token') 58 | self.mox.StubOutWithMock(postflight.flight_common, 'GetClientIdentifier') 59 | self.mox.StubOutWithMock( 60 | postflight.flight_common, 'GetRemainingPackagesToInstall') 61 | self.mox.StubOutWithMock(postflight.mac_client, 'SimianAuthClient') 62 | # mock out DictToStr because of hash randomization. 63 | self.mox.StubOutWithMock(postflight.flight_common, 'DictToStr') 64 | self.mox.StubOutWithMock( 65 | postflight.flight_common, 'UploadAllManagedInstallReports') 66 | self.mox.StubOutWithMock(postflight.munkicommon, 'cleanUpTmpDir') 67 | self.mox.StubOutWithMock(postflight, 'IsAppInPlace') 68 | self.mox.StubOutWithMock(postflight, 'NoteLastSuccess') 69 | 70 | postflight.flight_common.GetServerURL().AndReturn(mock_url) 71 | postflight.flight_common.GetAuth1Token().AndReturn('fake_auth') 72 | postflight.flight_common.GetClientIdentifier('auto').AndReturn(client_id) 73 | postflight.mac_client.SimianAuthClient( 74 | 'abcd4077-0b34-4572-ba91-cc7aad032b5c', 75 | hostname=mock_url).AndReturn(mock_client) 76 | mock_client.SetAuthToken('fake_auth') 77 | postflight.flight_common.GetClientIdentifier('auto').AndReturn(client_id) 78 | postflight.flight_common.GetRemainingPackagesToInstall().AndReturn(( 79 | pkgs_to_install, updates_to_install)) 80 | postflight.flight_common.DictToStr(client_id).AndReturn('fake_string') 81 | mock_client.PostReport('postflight', expected_report) 82 | postflight.flight_common.UploadAllManagedInstallReports( 83 | mock_client, client_id['on_corp']) 84 | postflight.IsAppInPlace().AndReturn(True) 85 | mock_client.LogoutAuthToken().AndReturn(True) 86 | postflight.munkicommon.cleanUpTmpDir() 87 | postflight.NoteLastSuccess().AndReturn(None) 88 | 89 | self.mox.ReplayAll() 90 | postflight.RunPostflight('auto') 91 | self.mox.VerifyAll() 92 | 93 | 94 | if __name__ == '__main__': 95 | basetest.main() 96 | -------------------------------------------------------------------------------- /src/tests/simian/mac/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Common constants/etc for Simian project.""" 3 | 4 | import re 5 | 6 | 7 | # Track name constants. 8 | STABLE = 'stable' 9 | TESTING = 'testing' 10 | UNSTABLE = 'unstable' 11 | TRACKS = [STABLE, TESTING, UNSTABLE] 12 | DEFAULT_TRACK = STABLE 13 | 14 | 15 | # Install type constants. 16 | MANAGED_INSTALLS = 'managed_installs' 17 | MANAGED_UNINSTALLS = 'managed_uninstalls' 18 | MANAGED_UPDATES = 'managed_updates' 19 | OPTIONAL_INSTALLS = 'optional_installs' 20 | INSTALL_TYPES = [ 21 | MANAGED_INSTALLS, MANAGED_UNINSTALLS, MANAGED_UPDATES, OPTIONAL_INSTALLS] 22 | DEFAULT_INSTALL_TYPE = MANAGED_INSTALLS 23 | 24 | # Manifest Modification Group Names 25 | MANIFEST_MOD_ADMIN_GROUP = 'admin' 26 | MANIFEST_MOD_SUPPORT_GROUP = 'support' 27 | MANIFEST_MOD_SECURITY_GROUP = 'security' 28 | MANIFEST_MOD_GROUPS = [ 29 | MANIFEST_MOD_SUPPORT_GROUP, 30 | MANIFEST_MOD_SECURITY_GROUP 31 | ] 32 | 33 | # Munki plist name allowed characters. 34 | PLIST_NAME_ALLOWED_CHAR_REGEX = r'[^\w\-\.]' 35 | 36 | 37 | def IsValidPlistName(name): 38 | """Verifies if a given plist name is valid. 39 | 40 | Args: 41 | name: str plist name. 42 | Returns: 43 | Boolean. True if the plist name is valid, False otherwise. 44 | """ 45 | if not name or re.search(PLIST_NAME_ALLOWED_CHAR_REGEX, name): 46 | return False 47 | return True 48 | 49 | 50 | def SanitizeUUID(uuid): 51 | """Sanitizes a UUID by lowercasing and removing any prepended "CN=" string. 52 | 53 | Args: 54 | uuid: str uuid. 55 | Returns: 56 | str uuid. 57 | """ 58 | uuid = uuid.lower() 59 | if uuid.startswith('cn='): 60 | uuid = uuid[3:] 61 | return uuid 62 | -------------------------------------------------------------------------------- /src/tests/simian/mac/common/testdata/applesus.sucatalog: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ApplePostURL 6 | http://swquery.apple.com/WebObjects/SoftwareUpdatesStats 7 | IndexDate 8 | 2011-06-21T12:30:32Z 9 | Products 10 | 11 | ID1 12 | 13 | PostDate 14 | 2007-02-22T23:36:54Z 15 | Packages 16 | 17 | 18 | URL 19 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/DVCPROHDCodec.tar 20 | Size 21 | 849920 22 | 23 | 24 | Distributions 25 | 26 | English 27 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/022-3271.English.dist 28 | 29 | 30 | ID2 31 | 32 | PostDate 33 | 2007-02-22T23:36:54Z 34 | Packages 35 | 36 | 37 | URL 38 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/DVCPROHDCodec.tar 39 | Size 40 | 849920 41 | 42 | 43 | Distributions 44 | 45 | English 46 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/022-3271.English.dist 47 | 48 | 49 | ID3 50 | 51 | PostDate 52 | 2007-02-22T23:36:54Z 53 | Packages 54 | 55 | 56 | URL 57 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/DVCPROHDCodec.tar 58 | Size 59 | 849920 60 | 61 | 62 | Distributions 63 | 64 | English 65 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/022-3271.English.dist 66 | 67 | 68 | ID4 69 | 70 | PostDate 71 | 2007-02-22T23:36:54Z 72 | Packages 73 | 74 | 75 | URL 76 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/DVCPROHDCodec.tar 77 | Size 78 | 849920 79 | 80 | 81 | Distributions 82 | 83 | English 84 | http://swcdn.apple.com/content/downloads/46/57/022-3271/rmqY4rJMxLgmYp8fMxJCfB22drKT5CMvxC/022-3271.English.dist 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/tests/simian/mac/common/testdata/testpackage.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | autoremove 6 | 7 | catalogs 8 | 9 | unstable 10 | 11 | description 12 | test package 13 | display_name 14 | testpackage 15 | installed_size 16 | 1 17 | installer_item_hash 18 | aaaabbbbccccddddeeeeffff 19 | installer_item_location 20 | testpackage.dmg 21 | installer_item_size 22 | 1 23 | minimum_os_version 24 | 10.5.0 25 | name 26 | testpackage 27 | receipts 28 | 29 | 30 | installed_size 31 | 1 32 | packageid 33 | com.google.corp.testpackage 34 | version 35 | 1 36 | 37 | 38 | uninstall_method 39 | removepackages 40 | uninstallable 41 | 42 | version 43 | 1 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/tests/simian/mac/cron/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/simian/mac/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """App Engine Models for Simian web application.""" 3 | 4 | 5 | # NOTE(user): Beware! Model module top level definitions can collide! 6 | 7 | # TODO(user): eventually, base should not be exposed. 8 | from simian.mac.models.base import * 9 | from simian.mac.models.munki import * 10 | from simian.mac.models.settings import * 11 | -------------------------------------------------------------------------------- /src/tests/simian/mac/models/settings_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """settings module tests.""" 18 | 19 | 20 | from google.apputils import app 21 | from google.apputils import basetest 22 | from simian.mac.models import settings 23 | 24 | 25 | class SettingsTest(basetest.TestCase): 26 | """Test Settings class.""" 27 | 28 | def testGetSettingsType(self): 29 | """Test GetSettingsType().""" 30 | self.assertEqual(settings.Settings.GetType('ca_public_cert_pem'), 'pem') 31 | self.assertEqual( 32 | settings.Settings.GetType('foo_ca_public_cert_pem'), 'pem') 33 | self.assertEqual(settings.Settings.GetType('unknown'), None) 34 | self.assertEqual(settings.Settings.GetType('email_reply_to'), 'string') 35 | 36 | 37 | def main(unused_argv): 38 | basetest.main() 39 | 40 | 41 | if __name__ == '__main__': 42 | app.run() 43 | -------------------------------------------------------------------------------- /src/tests/simian/mac/munki/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /src/tests/simian/mac/munki/handlers/catalogs_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Munki catalogs module tests.""" 18 | 19 | import httplib 20 | import logging 21 | 22 | 23 | import mock 24 | import stubout 25 | import webtest 26 | 27 | from google.appengine.ext import testbed 28 | 29 | from google.apputils import app 30 | from google.apputils import basetest 31 | 32 | from simian.mac import models 33 | from simian.mac.munki.handlers import catalogs 34 | from simian.mac.urls import app as gae_app 35 | 36 | 37 | @mock.patch.object(catalogs.auth, 'DoAnyAuth') 38 | class CatalogsHandlersTest(basetest.TestCase): 39 | 40 | def setUp(self): 41 | super(CatalogsHandlersTest, self).setUp() 42 | self.testbed = testbed.Testbed() 43 | 44 | self.testbed.activate() 45 | self.testbed.setup_env( 46 | overwrite=True, 47 | USER_EMAIL='user@example.com', 48 | USER_ID='123', 49 | USER_IS_ADMIN='0', 50 | DEFAULT_VERSION_HOSTNAME='example.appspot.com') 51 | 52 | self.testbed.init_all_stubs() 53 | self.testapp = webtest.TestApp(gae_app) 54 | 55 | def tearDown(self): 56 | super(CatalogsHandlersTest, self).tearDown() 57 | self.testbed.deactivate() 58 | 59 | def testGetSuccess(self, _): 60 | """Tests Catalogs.get().""" 61 | name = 'goodname' 62 | 63 | catalog_xml = '' 64 | models.Catalog(key_name=name, _plist=catalog_xml).put() 65 | 66 | resp = self.testapp.get('/catalogs/' + name, status=httplib.OK) 67 | self.assertTrue(resp.body.find('plist') != -1) 68 | 69 | def testGet404(self, _): 70 | """Tests Catalogs.get() where name is not found.""" 71 | name = 'badname' 72 | 73 | self.testapp.get('/catalogs/' + name, status=httplib.NOT_FOUND) 74 | 75 | 76 | logging.basicConfig(filename='/dev/null') 77 | 78 | 79 | def main(unused_argv): 80 | basetest.main() 81 | 82 | 83 | if __name__ == '__main__': 84 | app.run() 85 | -------------------------------------------------------------------------------- /src/tests/simian/mac/munki/handlers/icons_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import base64 18 | import httplib 19 | 20 | 21 | import mock 22 | import stubout 23 | import webtest 24 | 25 | import cloudstorage as gcs 26 | from google.apputils import app 27 | from google.apputils import basetest 28 | 29 | from simian import settings 30 | from simian.mac.common import auth 31 | from tests.simian.mac.common import test 32 | from simian.mac.urls import app as gae_app 33 | 34 | 35 | @mock.patch.object(auth, 'DoAnyAuth') 36 | class IconsModuleTest(test.AppengineTest): 37 | 38 | def setUp(self): 39 | super(IconsModuleTest, self).setUp() 40 | self.testapp = webtest.TestApp(gae_app) 41 | 42 | def testNotFound(self, *_): 43 | settings.ICONS_GCS_BUCKET = 'test' 44 | self.testapp.get('/icons/filename.png', status=httplib.NOT_FOUND) 45 | 46 | def testSuccess(self, *_): 47 | settings.ICONS_GCS_BUCKET = 'test' 48 | 49 | content = 'IMAGE_CONTENT' 50 | with gcs.open( 51 | '/test/%s.png' % base64.urlsafe_b64encode('Fname Dogfood'), 'w') as f: 52 | f.write(content) 53 | resp = self.testapp.get('/icons/Fname Dogfood.png', status=httplib.OK) 54 | 55 | self.assertEqual(content, resp.body) 56 | 57 | def testHashesPlist(self, _): 58 | self.testapp.get('/icons/_icon_hashes.plist', status=httplib.OK) 59 | 60 | 61 | def main(unused_argv): 62 | basetest.main() 63 | 64 | 65 | if __name__ == '__main__': 66 | app.run() 67 | -------------------------------------------------------------------------------- /src/tests/simian/mac/urls_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """urls module tests.""" 18 | 19 | import logging 20 | 21 | import re 22 | import types 23 | 24 | import tests.appenginesdk 25 | import mox 26 | import stubout 27 | 28 | from google.apputils import app 29 | from google.apputils import basetest 30 | from simian.mac import urls 31 | 32 | 33 | class SimianMainModuleTest(mox.MoxTestBase): 34 | """Test module level portions of urls.""" 35 | 36 | def setUp(self): 37 | mox.MoxTestBase.setUp(self) 38 | self.stubs = stubout.StubOutForTesting() 39 | 40 | def tearDown(self): 41 | self.mox.UnsetStubs() 42 | self.stubs.UnsetAll() 43 | 44 | def testStructure(self): 45 | """Test the overall structure of the module.""" 46 | self.assertTrue(hasattr(urls, 'app')) 47 | self.assertEqual( 48 | urls.webapp2.WSGIApplication, type(urls.app)) 49 | 50 | def testWgsiAppInitArgs(self): 51 | """Test the arguments that are supplied to setup the app var.""" 52 | 53 | def wsgiapp_hook(*args, **kwargs): 54 | o = self.mox.CreateMockAnything() 55 | o.set_by_test_hook = 1 56 | o.args = args 57 | o.kwargs = kwargs 58 | return o 59 | 60 | self.stubs.Set( 61 | urls.webapp2, 'WSGIApplication', wsgiapp_hook) 62 | self.mox.ReplayAll() 63 | reload(urls) 64 | app = urls.app 65 | self.assertNotEqual( 66 | urls.webapp2.WSGIApplication, type(app)) 67 | self.assertTrue(hasattr(app, 'set_by_test_hook')) 68 | self.assertTrue(type(app.args) is types.TupleType) 69 | self.assertTrue(type(app.args[0]) is types.ListType) 70 | self.assertTrue(type(app.kwargs) is types.DictType) 71 | 72 | for (regex, cls) in app.args[0]: 73 | _ = re.compile(regex) 74 | self.assertTrue(issubclass(cls, urls.webapp2.RequestHandler)) 75 | 76 | if 'debug' in app.kwargs: 77 | self.assertTrue(type(app.kwargs['debug']) is types.BooleanType) 78 | 79 | self.mox.VerifyAll() 80 | 81 | 82 | logging.basicConfig(filename='/dev/null') 83 | 84 | 85 | def main(unused_argv): 86 | basetest.main() 87 | 88 | 89 | if __name__ == '__main__': 90 | app.run() 91 | -------------------------------------------------------------------------------- /src/tests/simian/settings_datastore_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS-IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """settings module tests.""" 18 | 19 | import datetime 20 | import os 21 | import types 22 | 23 | 24 | import tests.appenginesdk 25 | from google.appengine.ext import testbed 26 | 27 | from google.apputils import app 28 | from google.apputils import basetest 29 | 30 | # pylint: disable=g-import-not-at-top 31 | # Set an environment variable so the settings module knows when it's being 32 | # tested directly, versus used for testing other modules. 33 | os.environ['____TESTING_SETTINGS_MODULE'] = 'yes' 34 | from simian import settings 35 | del os.environ['____TESTING_SETTINGS_MODULE'] 36 | 37 | from simian.mac import models 38 | # pylint: enable=g-import-not-at-top 39 | 40 | 41 | class DatastoreSettingsTest(basetest.TestCase): 42 | """Test DatastoreSettings.""" 43 | 44 | def setUp(self): 45 | super(DatastoreSettingsTest, self).setUp() 46 | 47 | self.testbed = testbed.Testbed() 48 | 49 | self.testbed.activate() 50 | self.testbed.setup_env( 51 | overwrite=True, 52 | USER_EMAIL='zerocool@example.com', 53 | USER_ID='123', 54 | USER_IS_ADMIN='1', 55 | DEFAULT_VERSION_HOSTNAME='example.appspot.com') 56 | 57 | self.testbed.init_all_stubs() 58 | 59 | if self.__class__.__name__ == 'BaseSettingsTestBase': 60 | return 61 | 62 | module_name = settings.DatastoreSettings.__name__ 63 | self.module = types.ModuleType(module_name) 64 | 65 | # Derive a class from this class that plugs in the test _Globals() 66 | # function. This makes testing more predictable as the contents 67 | # of the settings module may change at any time. 68 | class DerivedSettingsClass(settings.DatastoreSettings): 69 | 70 | def _Globals(xself): # pylint: disable=no-self-argument 71 | """Returns globals dict like globals().""" 72 | return {'FOO': 1} 73 | self.settings = DerivedSettingsClass(self.module) 74 | 75 | def tearDown(self): 76 | super(DatastoreSettingsTest, self).tearDown() 77 | self.testbed.deactivate() 78 | 79 | def testGetWhenDict(self): 80 | """Test _PopulateGlobals().""" 81 | self.assertEqual(self.settings.foo, 1) 82 | 83 | def testGetFromDatastore(self): 84 | value = 42 85 | models.Settings.SetItem('k', value) 86 | 87 | self.assertEquals(value, self.settings.k) 88 | 89 | def testSet(self): 90 | value = '423' 91 | self.settings.long_name = value 92 | 93 | v, stamp = models.Settings.GetItem('long_name') 94 | self.assertEquals(value, v) 95 | self.assertGreater( 96 | datetime.timedelta(seconds=1), datetime.datetime.utcnow() - stamp) 97 | 98 | def testDir(self): 99 | models.Settings.SetItem('some_extra_long_kEy', 343423) 100 | 101 | keys = dir(self.settings) 102 | 103 | self.assertIn('FOO', keys) 104 | self.assertIn('SOME_EXTRA_LONG_KEY', keys) 105 | 106 | 107 | def main(unused_argv): 108 | basetest.main() 109 | 110 | 111 | if __name__ == '__main__': 112 | app.run() 113 | --------------------------------------------------------------------------------