Skip to content
This repository has been archived by the owner on Jun 9, 2018. It is now read-only.

Commit

Permalink
Create LaunchD & add wait period check to close #4
Browse files Browse the repository at this point in the history
  • Loading branch information
clburlison committed Feb 19, 2016
1 parent 31803ea commit 6d23be5
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 150 deletions.
91 changes: 10 additions & 81 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,97 +4,26 @@ pinpoint
![pinpoint logo](/support_files/pinpoint-logo.png)



Provides location information on where a Mac is physical located.

The script uses Apple's CoreLocation framework to determine the approximate latitude, and longitude of a Mac.

Author: Clayton Burlison - https://clburlison.com


##Limitations
#Limitations
This module is limited to 10.8 - 10.11. On each run Location Services will be enabled, and the system Python binary will be given access to Location Services (LS). Due to how LS and the CoreLocation framework interact the first run to enable all the services has an exit after enabling Python plus LS. This exit will normally not be seen as Munki will automatically deploy the script and provide lookups on the second automatic munki run. The reason for this "delay" in activation is because the services need about 30 seconds to initialize the locationd daemon. However, munki's preflight script will timeout the script for waiting too long. After initial setup, sequential runs take about 6-8 seconds and have a pretty high accuracy.

###Google Geocoding API
We are using Google's Geocoding API for reverse address lookup. This API comes with the following limits:

* 2,500 free requests per day
* 10 requests per second

Additional information can be found: https://developers.google.com/maps/documentation/geocoding/usage-limits

###CoreLocation
CoreLocation requires a wireless adapter to obtain lookup information. On older Macs that did not ship with a wireless adapter this script will exit without enabling Location Services or authorizing Python for lookups. If your Mac has the wireless card disabled there is a chance that this script will still obtain a location based off of CoreLocation's cached data. Your Mac does not need to be connected to an active SSID for location data to be found just enabled.

Of interest:
> Your approximate location is determined using information from local Wi-Fi networks, and is collected by Location Services in a manner that doesn’t personally identify you.
>
> -- https://support.apple.com/en-us/HT204690
##Settings

The call out to Google's API can be disabled if you wish. If you disable this api call the only data provided to MunkiReport will be client side data from CoreLocation: Latitude, Longitude, Accuracy, and Altitude.

Disable Google API loopups:

```bash
sudo defaults write /Library/Preferences/MunkiReport ReportPrefs -dict-add location_address_lookup -bool False
````

Enable Google API lookups (default):
```bash
sudo defaults write /Library/Preferences/MunkiReport ReportPrefs -dict-add location_address_lookup -bool True
```

This can also be disabled with a profile. Example: [@clburlison/profiles](https://github.com/clburlison/profiles/blob/master/clburlison/MunkiReportDisableAddressLookups.mobileconfig).

##Errors

| Error message | Meaning |
|---|---|
| Your OS is not supported at this time | Your OS is either too old or possibly too new for this script. |
| Location Services was enabled. Please wait 30 seconds before doing a lookup | This status message should only be seen on the first run of this script. We need to wait 30 seconds before doing a lookup. |
| No wireless interface found | Without a wireless interface we cannot find geodata. |
| Unsuccessful: Unable to locate | CoreLocation was unable to locate your Mac. Apple Error Code: 3 |
| Unsuccessful: Denied | CoreLocation was denied access to location service. Apple Error Code: 2 |
| Unsuccessful: Restricted | CoreLocation obtained restricted access to location services. Apple Error Code: 1 |
| Unsuccessful: Not Determined | CoreLocation could not determine a geolocation. Apple Error Code: 0 |
| Unsuccessful: Location Services Disabled | Location Services is disabled. This is often due to your AirPort card being turned off. |
| Error obtaining a location. LS was unresponsive or a lookup timeout occurred. | General Error. Location services failed to find a location for one or more reasons. |

##Testing
In most cases the warnings produced by this script in a normal run will provide enough information to determine what the issue is. However, if you are unsure why a lookup is failing the best way to get more information is with the `verbose` output option.

To see warnings and information use:
```bash
sudo /usr/local/munki/preflight.d/location.py -v
```

To see a full output of warnings, information, and debug statements use:
```bash
sudo /usr/local/munki/preflight.d/location.py -vv
```

##Database entries

The following data is created by this script and can be accessed via MunkiReports API:
#Legal Notice
_coming soon_

* Address - Str, Estimated street address
* Latitude - Str, Latitude
* Longitude - Str, Longitude
* LatitudeAccuracy - Int, Latitude Accuracy
* LongitudeAccuracy - Int, Longitude Accuracy
* Altitude - Int, Altitude
* GoogleMap - Str, Pre-populated Google Maps URL
* LastRun - Str, Last run time stored in UTC time
* CurrentStatus - Str, Friendly message describing last run
* LS_Enabled - Bool, are Location Services enabled.

#Credits
Based off of works by:
@arubdesu - https://gist.github.com/arubdesu/b72585771a9f606ad800
@pudquick - https://gist.github.com/pudquick/c7dd1262bd81a32663f0
@pudquick - https://gist.github.com/pudquick/329142c1740500bd3797
@lindes - https://github.com/lindes/get-location/
University of Utah, Marriott Library - https://github.com/univ-of-utah-marriott-library-apple/privacy_services_manager
Munki, - https://github.com/munki/munki/blob/master/code/client/munkilib/FoundationPlist.py
[@arubdesu](https://github.com/arubdesu) - https://gist.github.com/arubdesu/b72585771a9f606ad800
[@pudquick](https://github.com/pudquick) - https://gist.github.com/pudquick/c7dd1262bd81a32663f0
[@pudquick](https://github.com/pudquick) - https://gist.github.com/pudquick/329142c1740500bd3797
[@lindes](https://github.com/lindes) - https://github.com/lindes/get-location/
[University of Utah, Marriott Library](https://github.com/univ-of-utah-marriott-library-apple) - https://github.com/univ-of-utah-marriott-library-apple/privacy_services_manager
[Munki](https://github.com/munki), - https://github.com/munki/munki/blob/master/code/client/munkilib/FoundationPlist.py
[Google Inc.](https://github.com/macops), https://github.com/munki/munki/blob/master/code/client/supervisor
175 changes: 112 additions & 63 deletions pkgroot/Library/Application Support/pinpoint/bin/pinpoint
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ and stores the location of your mac to a plist
by default.
"""

__author__ = 'Clayton Burlison (https://clburlison.com)'
__version__ = '0.0.1'

import sys
import os
import plistlib
Expand All @@ -19,9 +16,9 @@ import argparse
import logging
import json
from urllib2 import urlopen, URLError, HTTPError
from time import gmtime, strftime
from datetime import datetime, timedelta
from time import gmtime, strftime, strptime
import objc
sys.path.append('/usr/local/munki/munkilib/')
import FoundationPlist

# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
Expand All @@ -37,9 +34,14 @@ from Foundation import kCFPreferencesAnyUser
from Foundation import kCFPreferencesCurrentUser
from Foundation import kCFPreferencesCurrentHost

__author__ = 'Clayton Burlison (https://clburlison.com)'
__version__ = '0.0.1'

# Our preferences "bundle_id"
BUNDLE_ID = 'com.clburlison.pinpoint'

# Disable PyLint variable name checks
# pylint: disable=C0103
# Create global plist object for data storage
plist = dict()

Expand Down Expand Up @@ -92,7 +94,6 @@ def reload_prefs():
"""
CFPreferencesAppSynchronize(BUNDLE_ID)


def set_pref(pref_name, pref_value):
"""Sets a preference, writing it to
/Library/Preferences/com.clburlison.pinpoint.plist.
Expand All @@ -116,23 +117,60 @@ def pref(pref_name):
"""
default_prefs = {
'CacheDir': '/Library/Application Support/pinpoint',
'CheckWaitTime': 1800,
}
pref_value = CFPreferencesCopyAppValue(pref_name, BUNDLE_ID)
if pref_value == None:
if pref_value is None:
pref_value = default_prefs.get(pref_name)
# we're using a default value. We'll write it out to
# /Library/Preferences/<BUNDLE_ID>.plist for admin
# discoverability
set_pref(pref_name, pref_value)
if isinstance(pref_value, NSDate):
# convert NSDate/CFDates to strings
pref_value = str(pref_value)
return pref_value

def current_time_GMT():
def current_time():
"""Prints current date/time stamp"""
currentTime = strftime("%Y-%m-%d %H:%M:%S +0000", gmtime())
return currentTime
timeFormat = "%Y-%m-%d %H:%M:%S +0000"
now = strftime(timeFormat, gmtime())
return now

def check_wait_period(override):
"""Limit the amount of lookups a computer will run.
We want to use this in conjunction with out LaunchDaemon to
make sure we always have updated data available."""

# If we pass an override, skip this check
if override:
return True

# Setup and retrive date/time values for checking
now = convert_date_type(current_time())
LastCheckDate = pref('LastCheckDate')
CheckWaitTime = pref('CheckWaitTime')
last_run_time = convert_date_type(LastCheckDate)
try:
future_run_time = last_run_time + timedelta(seconds=CheckWaitTime)
if now >= future_run_time:
return True
else:
logging.warn("It is not time to run again! Use '-f' "
"if you wish to force an lookup.")
exit(1)
except TypeError:
return True

def convert_date_type(strDate):
"""Convert date string type to date type."""
try:
return datetime.strptime(strDate, "%Y-%m-%d %H:%M:%S +0000")
except TypeError:
pass

def update_check_time():
"""Record last check time"""
now = current_time()
logging.debug("Updating 'LastCheckDate' timestamp")
set_pref('LastCheckDate', now)

def write_to_cache_location(data, status):
"""Method to write plist data to disk"""
Expand All @@ -141,11 +179,13 @@ def write_to_cache_location(data, status):
plist = data
base_stats = dict(
CurrentStatus=str(status),
LastRun=str(current_time_GMT()),
LastRun=current_time(),
LS_Enabled=is_enabled,
)
plist.update(base_stats)
plist.update(base_stats)
output_file = os.path.join(pref('CacheDir'), "location.plist")
logging.debug("Writing current run details to: %s",
output_file)
plistlib.writePlist(plist, output_file)

def mac_has_wireless():
Expand Down Expand Up @@ -242,11 +282,8 @@ def add_python():
FoundationPlist.writePlist(clients_dict, das_plist)
os.chown(das_plist, 205, 205)
service_handler('load')
# We need to wait 30 secs for locationd service to initial on first setup.
status = "Location Services was enabled. Please wait 30 seconds before doing a lookup."
write_to_cache_location(None, status)
logging.warn(status)
exit(0)
logger.info("Location Services was enabled. We are waiting 30 seconds before doing a lookup.")
time.sleep(30)
else:
logging.info("Python is enabled")

Expand Down Expand Up @@ -347,7 +384,7 @@ def download_file(url):
except (OSError, IOError):
pass
return data

def address_resolve(lat, lon):
"""Use Google's Reverse GeoCoding API to resolve coordinates to an street address."""
try:
Expand Down Expand Up @@ -375,15 +412,17 @@ def munkireport_prefs():
return True

def main():
"""Locate Mac"""
parser = argparse.ArgumentParser(description="This script will \
enable Location Services and Python if your mac is supported. \
Python and the CoreLocation framework will process \
our lookup request finding your mac if possible.")
parser.add_argument('-v', '--verbose', action='count', default=0, help=" \
More verbose output. May be specified multiple times.")
parser.add_argument('-V', '--version', action='store_true', help=" \
Print script version")
"""Main method for handling options."""
parser = argparse.ArgumentParser(prog='pinpoint',
description='This script will attempt to locate your mac.')
parser.add_argument('--auto', action='store_true', default=True,
help='Used by the LaunchDaemon to find your mac in the background.')
parser.add_argument('-v', '--verbose', action='count', default=0,
help='More verbose output. May be specified multiple times.')
parser.add_argument('-f', '--force', action='store_true',
help='Force the location lookup run disregarding last check time.')
parser.add_argument('-V', '--version', action='store_true',
help='Print script version')
# args, unknown = parser.parse_known_args()
args = parser.parse_args()

Expand All @@ -397,43 +436,53 @@ def main():
print __version__
exit()

# This is the cache directory where we store our found location
cachedir = pref('CacheDir')
if not os.path.exists(cachedir):
os.makedirs(cachedir)

os_check()
if mac_has_wireless() is False:
status = "No wireless interface found."
write_to_cache_location(None, status)
logging.warn(status)
exit(0)
root_check()
sysprefs_boxchk()
add_python()
lookup(8)
global plist
try:
if munkireport_prefs() is True:
if plist['Latitude']:
add = dict(
Address=str(address_resolve(plist['Latitude'], plist['Longitude'])),
)
plist.update(add)
else:
add = dict(
Address=str('Address lookup has been disabled on this computer.'),
)
plist.update(add)
except:
pass
try:
write_to_cache_location(plist, plist['CurrentStatus'])
logging.info("Run status: %s", plist['CurrentStatus'])
except KeyError:
status = ("Error obtaining a location. LS was unresponsive "
"or a lookup timeout occurred.")
logging.warn(status)
write_to_cache_location(plist, status)
# Set default variable values for args
override = False

if args.force:
override = True

if args.auto:
os_check()
if mac_has_wireless() is False:
status = "No wireless interface found."
write_to_cache_location(None, status)
logging.warn(status)
exit(0)
root_check()
sysprefs_boxchk()
add_python()
if check_wait_period(override):
update_check_time()
lookup(8)
global plist
try:
if munkireport_prefs() is True:
if plist['Latitude']:
add = dict(
Address=str(address_resolve(plist['Latitude'], plist['Longitude'])),
)
plist.update(add)
else:
add = dict(
Address=str('Address lookup has been disabled on this computer.'),
)
plist.update(add)
except:
pass
try:
write_to_cache_location(plist, plist['CurrentStatus'])
logging.info("Run status: %s", plist['CurrentStatus'])
except KeyError:
status = ("Error obtaining a location. LS was unresponsive "
"or a lookup timeout occurred.")
logging.warn(status)
write_to_cache_location(plist, status)

if __name__ == '__main__':
main()
9 changes: 3 additions & 6 deletions pkgroot/Library/LaunchDaemons/com.clburlison.pinpoint.plist
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@
<string>--delayrandom</string>
<string>300</string>
<string>--timeout</string>
<string>300</string>
<string>180</string>
<string>--</string>
<string>/Library/Application Support/pinpoint/bin/pinpoint</string>
<string>--auto</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>10</integer>
</dict>
<key>StartInterval</key>
<integer>600</integer>
</dict>
</plist>

0 comments on commit 6d23be5

Please sign in to comment.